画面

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.memouseMemouseCallback と依存配列を書きます。書き忘れれば、無駄な再描画か、古い値を掴んだままの表示になります。どちらも実行してみるまで気付きません。

ここには、その種の指定がありません。再描画の速さのために書く行が、一行もありません。

ボタンに結ぶのはハンドラの名前

押されたときに何をするかは、ハンドラ名で指定します。ブロックは渡しません。

(row on: #click send: #select: with: o id)

#select: が自分に無ければコンパイルエラーです。イベントが運ぶ値にも型が付きます。値が要るときは on:sendValue:、イベントそのものを見たいときは on:sendEvent: を使い、e keye valuee scrollTop として読み出します。

自分が持っていない要素も聞けます。 ページの別の場所にあるボタンは、こう書いて聞きます。

me listen: '#back' on: #click send: #goBack.

ブロックを渡せないのは、書き方の好みではありません。VNode は別のアクタから要求されることがあり、ブロックは作られた場所を閉じ込めているので、そこを越えられないからです。

一覧の行には key を付ける

行を作るときに、その行を一意に決めるものを渡します。受注番号や社員番号です。

(Html clone: '#lines tr' fill: { ... }) key: o id

付けておくと、並べ替えや絞り込みで行の位置が変わっても、DOM の節点は作り直されずに移動します。行の中に入力欄があれば、打ちかけの文字もフォーカスも残ります。

付けなくても表示は正しくなります。位置で突き合わせて、中身を上書きするだけです。行の中に入力欄が無いなら、それで困りません。

同じ key を二つ付けるとエラーになります。 黙って直しません。直すと行が一つ消えて、しかもエラーが出ないからです。

数千行を超えたら、見えている分だけ描く

一描画にかかる時間は、画面に出ている行数に比例します。数百行なら全部描いて問題ありません。数千行を超えると重くなります。

そのときは、見えている行だけを描きます。言語の機能ではなく、画面の書き方です。 枠のスクロールを聞くと、scrollTopheight が届きます。行の高さは CSS で決めた定数なので、割れば何行目から何行が要るかが出ます。描かない分の高さは、上下の詰め物の行に持たせます。スクロールバーの長さはそれで正しくなります。

書き方は一万行の一覧にあります。1万行で、実際に描くのは二十数行、一描画は 4.9ms です。

毎フレーム描き直す画面も同じ書き方

アニメーションのように毎フレーム描き直したい場合も、書き方は変わりません。requestAnimationFrame を extern で四行宣言し、コールバックの中では自分にメッセージを投げます。

Screen >> tick = (
  Win requestAnimationFrame: [:t | me ref tell tick].
  ...次の状態を作る... ).

あとは他のハンドラと同じで、次の状態を返せば描かれます。ブラウザで測って毎秒60フレーム、そのすべてでアクタが描き直していました。

canvas に描く場合は view を使いません。canvas は描いた跡が残るので、毎フレーム自分で消してから描き直します。詳しくはゲームにあります。