Mithril 1.1.0

仮想DOMノード


仮想DOMとは何か

仮想DOMツリーはDOMツリーを表現するJavaScriptデータ構造です。ネストした仮想DOMノードはvnodesと呼ばれます。

仮想DOMが最初にレンダリングされる時はDOMツリーを作るためのブループリントとして使われます。

通常、仮想DOMツリーはレンダリングのサイクルのたびに再生成されます。レンダリングはイベントハンドラやデータの変更時に発生します。Mithrilは以前のバージョンのvnodeとの差分検知を行い、変更が必要なDOM要素に対してのみ変更を行います。

頻繁にvnodeを再作成するのは無駄に思えるかもしれませんが、現代のJavaScriptエンジンは1ミリ秒未満で数十万のオブジェクトを作成できます。一方、DOMを変更すると、vnodeを作成するよりも数桁上のコストがかかります。

そのため、Mithrilは高度に最適化された仮想DOMの差分検知アルゴリズムを使用し、DOMアップデートの量を最小限に抑えます。Mithrilはまた、JavaScriptエンジンがJITコンパイルしてネイティブに近いデータ構造アクセスのパフォーマンスが出せるように、慎重に設計されたvnodeデータ構造を使用しています。さらに、Mithrilはvnodeを作成する機能を積極的に最適化します。

Mithrilがこのように仮想DOMツリーを毎回生成するようなレンダリングモデルを採用するのは、保持モードレンダリング(retained mode rendering)をサポートするためです。このスタイルを採用することで、UIの複雑さを著しく下げます。

なぜ保持モードが大事なのかを説明するために、DOM APIとHTMLについてみてみましょう。DOM APIは直接モード(immediate mode)のレンダリングシステムとなっていて、DOMツリーを逐次構築するための命令を正確に組み立てる必要があります。DOM APIの特性としては、マイクロ最適化の機会はたくさんありますが、バグを作り込んでしまったり、理解しにくいコードになってしまうリスクもあります。

対象的に、HTMLのレンダリングシステムは保持モードです。HTMLではより自然で読みやすい方法でDOMツリーを書くことができます。子を親に追加し忘れたりもなく、とても深いツリーを書き出そうとしてスタックオーバーフローすることもありません。

仮想DOMはHTMLと非常に近い方法で、動的にDOMツリーを構築できます。データの変更に対して効率よくUIを更新するためにDOM API呼び出しを駆使する必要はありません。


基礎

仮想DOMノード、あるいはvnodeはDOMエレメント、あるいはDOMの一部を現すJavaScriptのオブジェクトです。Mithrilの仮想DOMエンジンはvnodeのツリーを受け取って、DOMツリーを生成します。

vnodeはm() hyperscriptユーティリティを使って作られます:

m("div", {id: "test"}, "hello")

hyperscriptの入力にはcomponentを与えることができます:

// コンポーネント定義
var ExampleComponent = {
    view: function(vnode) {
        return m("div", vnode.attrs, ["Hello ", vnode.children])
    }
}

// 利用する
m(ExampleComponent, {style: "color:red;"}, "world")

// 等価なHTML:
// <div style="color:red;">Hello world</div>

構造

仮想DOMノード、あるいはvnodeはDOMエレメント、あるいはDOMの一部を現すJavaScriptのオブジェクトで、次のようなプロパティを持っています:

プロパティ 説明
tag String|Object DOMエレメントのノード名(nodeName)。vnodeがフラグメントの時はこの名前が[に、テキストvnodeでは#に、信頼されたHTMLだと<となります。さらにコンポーネントを設定することもできます。
key String? 配列のデータの中のアイテムを表現するためのDOMエレメントと、元のデータを関連付けるためのデータです。
attrs Object? DOM属性, イベント, プロパティ, ライフサイクルメソッドを保持するハッシュマップです。
children (Array|String|Number|Boolean)? ほとんどのvnodeのタイプでは、childrenプロパティはvnodeの配列です。テキストや信頼されたHTMLのvnodeでは、childrenプロパティは文字列や数値、bool値です。
text (String|Number|Boolean)? vnodeがテキストノードのみを子供として持つ時は、childrenの代わりに使用されます。これはパフォーマンス上の理由からこのようになっています。Componentのvnodeの場合は、テキストノードの子しか持っていなくてもtextプロパティが使われることはありません。
dom Element? vnodeに関連するエレメントの参照を保持します。oninitライフサイクルメソッドではこのプロパティはundefinedです。フラグメントや信頼されたHTMLのvnodeではdomは関連する範囲のノードの最初のエレメントです。
domSize Number? これはフラグメントや信頼されたHTMLのvnodeでのみセットされます。他のノードではundefinedです。このプロパティはdomプロパティを含めて、vnodeが表現するDOMエレメントがいくつあるのかを示します。
state Object? 再描画のサイクル間でステータスを永続化するオブジェクトです。このプロパティはコアのエンジンが設定します。POJOコンポーネントのvnodeでは、stateはコンポーネントのオブジェクト/クラスをプロトタイプ継承します。クラスコンポーネントのvnodeはクラスのインスタンスです。クロージャーコンポーネントの場合は、クロージャーが返すオブジェクトです。
_state Object? オリジナルのvnode.stateオブジェクトへの参照です。viewやフックを探すのに使われます。このプロパティはMithrilが内部的に使うもので変更してはいけません。
events Object? このオブジェクトは再描画間で、DOM APIを使って削除される可能性のあるイベントハンドラを永続化します。イベントハンドラを持たない型の場合は、eventsプロパティはundefinedです。このプロパティはMithrilが内部的に使うもので変更してはいけません。
instance Object? コンポーネントのviewが返す値を保存していおくものです。このプロパティはMithrilが内部的に使うもので変更してはいけません。
skip Boolean このプロパティはMithrilがキーが設定されたリストの差分検知時に内部的に使うもので、変更してはいけません。

Vnodeタイプ

vnodeのtagプロパティは型を決定します。5種類のvnodeの型があります:

vnode種類 サンプル 説明
Element {tag: "div"} DOMエレメント表現
Fragment {tag: "[", children: []} DOMエレメントのリストです。このリストの親はフラグメント以外のエレメントを含む可能性があります。m()ヘルパー関数を使うと、入れ子になったchildrenパラメーターがm()にあるとフラグメントvnodeが作られる可能性があります。m("[")は不正なvnodeを作ります。
Text {tag: "#", children: ""} DOMテキストノードを表現します。
信頼されたHTML {tag: "<", children: "<br>"} HTML文字列を含むDOMエレメントのリストを表現します。
Component {tag: ExampleComponent} tagviewメソッドを持つJavaScriptオブジェクトであれば、vnodeはコンポーネントによってレンダリングされたDOMを表現します。

仮想DOMツリーに格納されているすべてのものは、テキストも含めてvnodeです。m()ユーティリティは自動的にchildren引数を正規化し、テキストはテキストノードに、ネストされた配列はフラグメントvnodeにします。

m()関数の最初の引数には、タグ名かコンポーネントだけが設定可能です。いいかえると、[, #, <m()selector引数として正しくありません。信頼されたHTMLのvnodeはm.trust()を使って作られます。


単相クラス

mithril/render/vnodeモジュールはMithrilがすべてのvnodeを作る時に使用するメソッドです。これにより、現代のJavaScriptエンジンは、vnodeを常に同じ隠しクラスにコンパイルすることによって、仮想DOMの差分計算を最適化できます。

vnodeを発行するライブラリを作成するときは、裸のJavaScriptオブジェクトを記述するのではなく、このモジュールを使うことで、高いレベルのレンダリングパフォーマンスを維持できます。


アンチパターンを避ける

変更可能なvnodeをメモリ中に保存するのを避ける

vnodeはある特定の瞬間のDOMの状態を表現するスナップショットです。Mithrilのレンダリングエンジンは、vnodeは再利用の有無にかかわらず変更されていないものとして扱います。前のレンダリングで使ったvnodeを変更した時の動作は未定義です。

vnodeを再利用することで差分検知を抑制することができますが、onbeforeupdateフックを使った方が他のプログラマーや自分自身に対して明確に意図を伝えることができます。


License: MIT. © Leo Horie.