画面
HTML は書かず、ページから複製します
画面のプログラムに HTML は書きません。デザイナが書いたページから、必要な部分を複製して値を入れます。
Screen >> rowOf: o =
(Html clone: '#lines tr' fill: {
'.c-id' -> o id.
'.c-customer' -> o customer.
'.c-total' -> (self yen: o total) })
key: o id.
#lines tr は、デザイナが表の見た目を確かめるために置いた見本の行です。それを一件ごとに複製し、.c-id などの位置に値を入れます。
デザイナが列を足しても、こちらで直す場所はありません。 class 名が変わったときは、組み立てが止まります。モジュールの先頭にどのページのものかを書いてあり、セレクタはそのファイルと突き合わせるからです。
module Orders page: 'index.html'.
画面を開いてから「値がどこにも入っていない」と気付くことはありません。
項目が数百あっても、行数は増えません
一項目につき一行を書く必要はありません。JSON の鍵とページの class 名を規則で結べば、まとめて入れられます。
Screen >> view = self reply: (
data keys inject: (Html clone: '#card') into: [:node :k |
node at: ('.f-' , k) put: ((data at: k) asStr orElse: [(data at: k) asText]) ]).
項目が6つでも300でも、書くのはこの三行です。
描き直しの指定は書きません
どこを描き直すかを書く場所がありません。 依存を並べる配列も、記憶化の指定も、購読の宣言もありません。書かないので、書き忘れることもありません。
なぜ指定が要らないのか
ハンドラは次の状態を返します。だから、状態が変わったかどうかは、返ってきた値と今の状態を比べれば分かります。
Screen >> search: text = self with: { query: text }. "変わった。描く"
Screen >> ping = self. "変わらない。描かない"
比べているのは、返ってきた値が今の状態と同じものかどうかです。中身を全部見比べるわけではありません。値は変化しないので、with: で作った新しい状態は必ず別のもの、self は必ず同じものになります。
判断しているのはここ一箇所です。画面のどこにも書かれていません。
三つ覚えれば、それで全部です
変えたくないときは self を返します。 描き直しは起きません。
子アクタは自分の区画を持ちます。 親を描き直しても、子には降りていきません。親が持っているのは子への参照で、参照は変わっていないからです。一覧の行を子アクタにすれば、親の再描画は行に届きません。
一覧の行には key: を付けます。 節点が作り直されずに移動するので、行の中の入力欄が生き残ります。
覚えることはこれだけです。memo を付けたか、依存配列に何を入れたか、購読を解除したかを気にする場面はありません。
実際に効きます
ゲームを作ったときに、点数のラベルへ毎フレーム同じ文字を送っていたことがあります。ラベルは毎回新しい状態を返していたので、毎秒60回、同じ内容を DOM に書いていました。最悪のフレーム間隔は 48.2ms まで伸びていました。
ラベルを「文字が同じなら self を返す」形に直したら、18.8ms に戻りました。直したのは一行です。 依存の指定を足したのではなく、状態が変わらないと言っただけです。
何が無いのか
React では React.memo、useMemo、useCallback と依存配列を書きます。書き忘れれば、無駄な再描画か、古い値を掴んだままの表示になります。どちらも実行してみるまで気付きません。
ここには、その種の指定がありません。再描画の速さのために書く行が、一行もありません。
ボタンに結ぶのはハンドラの名前
押されたときに何をするかは、ハンドラ名で指定します。ブロックは渡しません。
(row on: #click send: #select: with: o id)
#select: が自分に無ければコンパイルエラーです。イベントが運ぶ値にも型が付きます。値が要るときは on:sendValue:、イベントそのものを見たいときは on:sendEvent: を使い、e key や e value や e scrollTop として読み出します。
自分が持っていない要素も聞けます。 ページの別の場所にあるボタンは、こう書いて聞きます。
me listen: '#back' on: #click send: #goBack.
ブロックを渡せないのは、書き方の好みではありません。VNode は別のアクタから要求されることがあり、ブロックは作られた場所を閉じ込めているので、そこを越えられないからです。
一覧の行には key を付ける
行を作るときに、その行を一意に決めるものを渡します。受注番号や社員番号です。
(Html clone: '#lines tr' fill: { ... }) key: o id
付けておくと、並べ替えや絞り込みで行の位置が変わっても、DOM の節点は作り直されずに移動します。行の中に入力欄があれば、打ちかけの文字もフォーカスも残ります。
付けなくても表示は正しくなります。位置で突き合わせて、中身を上書きするだけです。行の中に入力欄が無いなら、それで困りません。
同じ key を二つ付けるとエラーになります。 黙って直しません。直すと行が一つ消えて、しかもエラーが出ないからです。
数千行を超えたら、見えている分だけ描く
一描画にかかる時間は、画面に出ている行数に比例します。数百行なら全部描いて問題ありません。数千行を超えると重くなります。
そのときは、見えている行だけを描きます。言語の機能ではなく、画面の書き方です。 枠のスクロールを聞くと、scrollTop と height が届きます。行の高さは CSS で決めた定数なので、割れば何行目から何行が要るかが出ます。描かない分の高さは、上下の詰め物の行に持たせます。スクロールバーの長さはそれで正しくなります。
書き方は一万行の一覧にあります。1万行で、実際に描くのは二十数行、一描画は 4.9ms です。
毎フレーム描き直す画面も同じ書き方
アニメーションのように毎フレーム描き直したい場合も、書き方は変わりません。requestAnimationFrame を extern で四行宣言し、コールバックの中では自分にメッセージを投げます。
Screen >> tick = (
Win requestAnimationFrame: [:t | me ref tell tick].
...次の状態を作る... ).
あとは他のハンドラと同じで、次の状態を返せば描かれます。ブラウザで測って毎秒60フレーム、そのすべてでアクタが描き直していました。
canvas に描く場合は view を使いません。canvas は描いた跡が残るので、毎フレーム自分で消してから描き直します。詳しくはゲームにあります。