Virtual DOM ノード
Virtual DOM とは
Virtual DOM ツリーは、DOM ツリーを表現する JavaScript のデータ構造です。これは、ネストされた Virtual DOM ノード、別名 vnode で構成されています。
Virtual DOM ツリーが最初にレンダリングされる際、その構造に一致する DOM ツリーを作成するための設計図として使用されます。
通常、Virtual DOM ツリーは、イベントハンドラーやデータの変更に応じて、レンダリングサイクルごとに再作成されます。Mithril.js は、vnode ツリーを以前のバージョンと比較(差分検出)し、変更があった箇所でのみ DOM 要素を更新します。
vnode を頻繁に再作成するのは非効率に思えるかもしれませんが、最新の JavaScript エンジンは 1 ミリ秒未満で数十万のオブジェクトを生成できます。一方、DOM の変更は vnode の作成よりも数桁コストが高くなります。
そのため、Mithril.js は、高度に最適化された Virtual DOM 差分アルゴリズムを使用して、DOM の更新量を最小限に抑えます。Mithril.js はさらに、JavaScript エンジンによってコンパイルされ、ネイティブに近いデータ構造アクセスパフォーマンスを実現するために、注意深く設計された vnode データ構造を生成します。さらに、Mithril.js は vnode を作成する関数も積極的に最適化します。
Mithril.js がレンダリングごとに Virtual DOM ツリー全体を再作成するレンダリングモデルをサポートするために、これほど多大な努力を払っているのは、宣言的な immediate mode API を提供するためです。この API は、UI の複雑さを大幅に管理しやすくするレンダリングスタイルです。
immediate mode が非常に重要な理由を説明するために、DOM API と HTML について考えてみましょう。DOM API は命令型の retained mode API であり、1. DOM ツリーを手順的に組み立てるための正確な命令を記述すること、および 2. そのツリーを更新するための命令を記述することが必要です。DOM API の命令的な性質は、コードをマイクロ最適化する機会が多いことを意味しますが、バグが発生する可能性が高くなり、コードを理解するのが難しくなる可能性も高くなります。
対照的に、HTML は immediate mode レンダリングシステムに近いものです。HTML を使用すると、子を親に追加するのを忘れたり、非常に深いツリーをレンダリングするときにスタックオーバーフローが発生したりすることを心配せずに、より自然で読みやすい方法で DOM ツリーを記述できます。
Virtual DOM は、UI を任意のデータ変更に効率的に同期するために、複数の DOM API 呼び出しを手動で記述することなく、動的な DOM ツリーを記述できるようにすることで、HTML よりもさらに一歩進んでいます。
基本
Virtual DOM ノード、すなわち vnode は、DOM 要素(または DOM の一部)を表す JavaScript オブジェクトです。Mithril.js の Virtual DOM エンジンは、vnode のツリーを受け取り、DOM ツリーを生成します。
Vnode は、m()
ユーティリティを使用して作成されます。
m('div', { id: 'test' }, 'hello');
Hyperscript は、コンポーネントも受け取ることができます。
// コンポーネントを定義する
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>
構造
Virtual DOM ノード、または vnode は、要素(または DOM の一部)を表す JavaScript オブジェクトで、次のプロパティを持ちます。
プロパティ | 型 | 説明 |
---|---|---|
tag | String|Object | DOM 要素の nodeName 。vnode がフラグメントの場合は文字列 [ 、テキスト vnode の場合は # 、信頼できる HTML vnode の場合は < になることもあります。さらに、コンポーネントを指定することもできます。 |
key | String? | DOM 要素をデータの配列内の各項目に対応付けるために使用される値。 |
attrs | Object? | DOM 属性、イベント、プロパティ、および ライフサイクルメソッド のハッシュマップ。 |
children | (Array|String|Number|Boolean)? | ほとんどの vnode タイプでは、children プロパティは vnode の配列です。テキストおよび信頼できる HTML vnode の場合、children プロパティは文字列、数値、またはブール値です。 |
text | (String|Number|Boolean)? | vnode にテキストノードが唯一の子として含まれている場合、children の代わりに使用されます。これはパフォーマンス上の理由によるものです。コンポーネント vnode は、テキストノードが唯一の子として含まれている場合でも、text プロパティを使用しません。 |
dom | Element? | vnode に対応する要素への参照。このプロパティは、oninit ライフサイクルメソッドでは undefined です。フラグメントおよび信頼できる HTML vnode では、dom は範囲内の最初の要素を指します。 |
domSize | Number? | これはフラグメントおよび信頼できる HTML vnode でのみ設定され、他のすべての vnode タイプでは undefined です。これは、vnode が表す DOM 要素の数(dom プロパティによって参照される要素から始まる)を定義します。 |
state | Object? | 再描画間で保持されるオブジェクト。必要に応じてコアエンジンによって提供されます。POJO コンポーネント vnode では、state はコンポーネントオブジェクト/クラスから原型的に継承されます。クラスコンポーネント vnode では、クラスのインスタンスです。クロージャコンポーネントでは、クロージャによって返されるオブジェクトです。 |
events | Object? | 再描画間で保持され、DOM API を使用して削除できるようにイベントハンドラーを格納するオブジェクト。イベントハンドラーが定義されていない場合、events プロパティは undefined です。このプロパティは Mithril.js 内部でのみ使用され、使用または変更しないでください。 |
instance | Object? | コンポーネントの場合、view によって返される値のストレージの場所。このプロパティは Mithril.js 内部でのみ使用され、使用または変更しないでください。 |
Vnode タイプ
vnode の tag
プロパティが、そのタイプを決定します。vnode タイプは 5 つあります。
Vnode タイプ | 例 | 説明 |
---|---|---|
要素 | {tag: "div"} | DOM 要素を表します。 |
フラグメント | {tag: "[", children: []} | 親 DOM 要素に、フラグメントに属さない他の要素も含まれている可能性がある DOM 要素のリストを表します。m() ヘルパー関数を使用する場合、フラグメント vnode は、m() の children パラメーターに配列をネストすることによってのみ作成できます。m("[") は有効な vnode を作成しません。 |
テキスト | {tag: "#", children: ""} | DOM テキストノードを表します。 |
信頼できる HTML | {tag: "<", children: "<br>"} | HTML 文字列から生成された DOM 要素のリストを表します。 |
コンポーネント | {tag: ExampleComponent} | tag が view メソッドを持つ JavaScript オブジェクトの場合、vnode はコンポーネントのレンダリングによって生成された DOM を表します。 |
Virtual DOM ツリー内のすべては vnode であり、テキストも含まれます。m()
ユーティリティは、children
引数を自動的に正規化し、文字列をテキスト vnode に、ネストされた配列をフラグメント vnode に変換します。
要素タグ名とコンポーネントのみが、m()
関数の最初の引数になることができます。言い換えれば、[
、#
、および <
は、m()
の有効な selector
引数ではありません。信頼できる HTML vnode は、m.trust()
を使用して作成できます。
単一型クラス
mithril/render/vnode
モジュールは、Mithril.js によってすべての vnode を生成するために使用されます。これにより、最新の JavaScript エンジンは、vnode を常に同じ隠しクラスにコンパイルすることで、Virtual DOM の差分処理を最適化できます。
vnode を出力するライブラリを作成する場合は、高いレベルのレンダリングパフォーマンスを確保するために、素の JavaScript オブジェクトを記述する代わりに、このモジュールを使用する必要があります。
アンチパターンを避ける
vnodeの再利用を避ける
vnodeはある時点でのDOMの状態を表すものです。Mithril.jsのレンダリングエンジンは、再利用されたvnodeは変更されていないと仮定するため、以前のレンダリングで使用されたvnodeを変更すると、未定義の動作が発生します。
差分を避けるためにvnodeをその場で再利用することは可能ですが、onbeforeupdate
を使用することをお勧めします。
モデルデータを属性経由でコンポーネントに直接渡すことを避ける
key
プロパティが、Mithril.jsのキーロジックと競合する形でデータモデルに現れる可能性があります。また、モデル自体が、onupdate
やonremove
のようなライフサイクルフックと同じ名前のメソッドを持つ可変インスタンスである可能性もあります。たとえば、モデルがカスタマイズ可能なカラーキーを表すためにkey
プロパティを使用する場合があります。これが変更されると、コンポーネントが間違ったデータを受け取ったり、予期せず位置が変わったり、その他の予期しない不要な動作が発生する可能性があります。代わりに、属性として渡すことで、Mithril.jsが誤って解釈しないようにします(また、後でそれを変更したり、プロトタイプメソッドを呼び出したりすることも可能です)。
// データモデル
var users = [
{ id: 1, name: 'John', key: 'red' },
{ id: 2, name: 'Mary', key: 'blue' },
];
// 後で...
users[0].key = 'yellow';
// 避ける
users.map(function (user) {
// John のコンポーネントは破棄され、再作成されます
return m(UserComponent, user);
});
// 推奨
users.map(function (user) {
// Key が明示的に抽出されます: データモデルには独自のプロパティが与えられます
return m(UserComponent, { key: user.id, model: user });
});
ビューメソッドでのステートメントの回避
ビューメソッド内のJavaScriptステートメントは、HTMLツリーの自然なネスト構造を変更する必要があることが多く、コードが冗長になり、読みにくくなります。
// 避ける
var BadListComponent = {
view: function (vnode) {
var list = [];
for (var i = 0; i < vnode.attrs.items.length; i++) {
list.push(m('li', vnode.attrs.items[i]));
}
return m('ul', list);
},
};
代わりに、条件付きレンダリングには三項演算子、リストのような構造には Array メソッドなどの JavaScript の式を使用することをお勧めします。
// 推奨
var BetterListComponent = {
view: function (vnode) {
return m(
'ul',
vnode.attrs.items.map(function (item) {
return m('li', item);
})
);
},
};