仮想 DOM ライブラリの Snabbdom の README からコアとモジュール、ヘルパーに関する解説を翻訳しました (2016年3月4日時点)。
コア
Snabbdom のコアはもっとも必要不可欠な機能のみを提供します。速く拡張性を保ちながら、なるべくシンプルであるように設計されています。
snabbdom.init
コアは唯一で単独の関数である snabbdom.init
だけを公開します。これはモジュールのリストを引数にとり、指定されたモジュールのセットを使う patch 関数を返します。
var patch = snabbdom.init([
require('snabbdom/modules/class'),
require('snabbdom/modules/style'),
]);
patch
init
によって返される patch
関数は2つの引数をとります。第1引数は現在のビューをあらわす DOM 要素もしくは vnode です。第2引数は新しく更新されるビューをあらわす vnode です。
親をもつ DOM 要素が渡される場合、newVnode は DOM ノードに返され、渡された要素はつくられた DOM ノードに置き換わります、古い vnode が渡された場合、新しい vnode での記述にマッチするように、Snabbdom はそれを効率的に修正します。
渡される古い vnode は以前のパッチ呼び出しからの vnode の結果をあらわしていなければなりません。Snabbdom が情報を vnode に保存するからです。このことによって、よりシンプルで高性能なアーキテクチャを実装することが可能になります。これによって、古い vnode ツリーをあらたにつくることが避けられます。
patch(oldVnode, newVnode);
snabbdom/h
VNode をつくるには snabbdom/h
を使うことをおすすめします。h
はタグもしくはセレクターを文字列として受け取ります。また、オプションのデータオブジェクトとオプションの文字列もしくは子要素達の配列をとります。
var h = require('snabbdom/h');
var vnode = h('div', {style: {color: '#000'}}, [
h('h1', 'Headline'),
h('p', 'A paragraph'),
]);
フック
フックは DOM ノードのライフサイクルに接続するための方法です。Snabbdom はフックの豊富な選択肢を提供します。フックは Snabbdom を拡張するためにモジュールによって使われるほか、仮想ノードのライフの望む時点で任意のコードを実行するために通常のコードに対しても使われます。
概要
名前 | 作動するとき | コールバックの引数 |
---|---|---|
pre |
パッチプロセスが始めるとき | none |
init |
vnode が追加されたとき | vnode |
create |
VNode にもとづいて DOM 要素がつくられたとき | emptyVNode, vnode |
insert |
要素が DOM に挿入されるとき | vnode |
prepatch |
要素にパッチが適用される前 | oldVnode, vnode |
update |
要素が更新されるとき | oldVnode, vnode |
postpatch |
要素にパッチが適用された後 | oldVnode, vnode |
destroy |
要素が直接もしくは間接的に削除されるとき | vnode |
remove |
DOM から要素が直接削除されるとき | vnode, removeCallback |
post |
パッチプロセスが終了したとき | none |
次のフックはモジュールに対して利用できます: pre
、create
、 update
、destroy
、remove
、post
次のフックは個別要素のフックプロパティで利用できます: init
、create
、insert
、prepatch
、update
、 postpatch
、destroy
、remove
使い方
フックを使うには、データオブジェクト引数のフィールドをフックするためのオブジェクトとしてそれらを渡します。
h('div.row', {
key: movie.rank,
hook: {
insert: (vnode) => { movie.elmHeight = vnode.elm.offsetHeight; }
}
});
init フック
パッチが処理されているあいだで新しい仮想ノードが見つかったときに、このフックが起動します。このフックは Snabbdom が何らかの方法でノードを処理する前にこのフックが呼び出されます。すなわち、vnode にもとづき DOM ノードがつくられる前です。
フックハンドラーが vnode の vnode プロパティをセットする場合、Snabbdom は実際の vnode の代わりに vnode での vnode (vnode at vnode) を使います。
insert フック
vnode への DOM 要素が document に挿入され、パッチサイクルの残りが終了したときにこのフックが起動します。このことは挿入された要素の位置に影響を及ぼす要素が変更されず、安全であることを前提に、(このフックで getBoundingClientRect を使うことのように) DOM の測定を実行できることを意味します。
remove フック
要素の削除に入り込むことを可能にします。DOM から vnode が駆除されようとするときに、このフックは一度呼び出されます。ハンドリング関数は vnode とコールバックの両方を受け取ります。コールバックで削除をコントロールしたり遅延させることができます。フックが自身の責務を実行し終えたときにこれは起動します。すべての remove フックがコールバックを実行した後にその要素だけが削除されます。
親から要素が削除されるときのみフックが削除されます。削除される要素の子要素の場合は該当しません。そのためには、destroy フックをご参照ください。
destroy フック
DOM から DOM 要素が削除されるもしくは DOM 要素の親が DOM から削除されるときにこのフックが起動します。
このフックと remove フックの違いを見るには、次の例を見てみましょう。
var vnode1 = h('div', [h('div', [h('span', 'Hello')])]);
var vnode2 = h('div', []);
patch(container, vnode1);
patch(vnode1, vnode2);
destroy フックはそれを含む内側の div 要素と span 要素の両方に対して発動します。一方で remove は div 要素に対してのみ発動します。この要素が親要素から離れた唯一の要素だからです。
たとえば、要素が削除されるときにアニメーションを発動させるために使うことができます。また、削除された要素の子孫が消える要素をアニメーションとして追加するために destroy フックを使うことができます。
モジュールを作成する
フックに対するグローバルリスナーを登録することで、モジュールは機能します。モジュールはフックの名前から関数までの単純なディクショナリです。
var myModule = {
create: function(oldVnode, vnode) {
// 新しい仮想ノードがつくられたときに起動する
},
update: function(oldVnode, vnode) {
// 仮想ノードが更新されたときに起動する
}
};
このメカニズムによって、Snabbdom のふるまいをかんたんに augument できます。デモンストレーションとして、デフォルトモジュールの実装を見てみましょう。
モジュール
この説ではコアモジュールを説明します。すべてのモジュールはオプションです。
class モジュール
class モジュールは要素上のクラスを動的に切り替えるためのかんたんな方法を提供します。class
データプロパティでオブジェクトが期待されます。オブジェクトは VNode でのクラスがとどまるもしくは切り替えることを示すブール値にクラス名をマップします。
h('a', {class: {active: true, selected: false}}, 'Toggle');
props
モジュール
DOM 要素のプロパティをセットすることができます。
h('a', {props: {href: '/foo'}}, 'Go to Foo');
attributes
モジュール
props と同じですが、DOM 要素のプロパティの代わりに属性をセットします。
h('a', {attrs: {href: '/foo'}}, 'Go to Foo');
属性の追加と更新には setAttribute を使います。以前追加もしくはセットされたものの現在は sttrs オブジェクトに存在しない属性に関して、removeAttribute を使って DOM要素の属性リストから削除されます。
ブール値の属性の場合 (たとえば、disabled、hidden、selected ...)、実現方法 (the meaning) は属性の値 (true もしくは false) に依存しませんが、DOM 要素での属性自身の存在/不在に依存します。これらの属性はモジュールによって異なる扱いを受けます。boolean 属性が falsy な値にセットされている場合 (0、-0、null、false、NaN、undefined、もしくは空の文字列 (""))、その属性は DOM 要素の属性リストから削除されます。
style
モジュール
style モジュールは HTML の見た目をなめらかにしてアニメーションをスムーズにするためにあります。コアにおいて、エレメントで CSS プロパティをセットすることができます。
h('span', {
style: {border: '1px solid #bada55', color: '#c0ffee', fontWeight: 'bold'}
}, 'Say my name, and every colour illuminates');
スタイルオブジェクトからプロパティとしてスタイル属性が削除される場合、style モジュールは style 属性を削除しないことにご注意ください。スタイルを削除するには、代わりに空の文字をセットします。
h('div', {
style: {position: shouldFollow ? 'fixed' : ''}
}, 'I, I follow, I follow you');
delayed プロパティ
プロパティを評価するように指定することができます。これらのプロパティが変化したとき、次のフレームの後まで変化は適用されません。
h('span', {
style: {opacity: '0', transition: 'opacity 1s', delayed: {opacity: '1'}}
}, 'Imma fade right in!');
remove でプロパティをセットする
要素がDOM から削除されようとするとき、remove プロパティにセットされたスタイルは効果を発揮します。適用されたスタイルは CSS トランジションでアニメーションが実行されます。すべてのスタイルのアニメーションが実行されたときのみ、DOM から要素が削除されます。
h('span', {
style: {opacity: '1', transition: 'opacity 1s',
remove: {opacity: '0'}}
}, 'It\'s better to fade out than to burn away');
これによって、要素の削除のアニメーションを宣言的に実行することがかんたんになります。
destroy でプロパティをセットする
h('span', {
style: {opacity: '1', transition: 'opacity 1s',
destroy: {opacity: '0'}}
}, 'It\'s better to fade out than to burn away');
eventlisteners モジュール
event listeners モジュールはイベントリスナーをアタッチするための強力な機能を提供します。
リスニングしたいイベントの名前に対応するプロパティをもつオブジェクトを提供することで、VNode のイベントに関数をアタッチすることができます。イベントが起きたときに関数が呼びだされ、イベントオブジェクトが渡されます。
function clickHandler(ev) { console.log('got clicked'); }
h('div', {on: {click: clickHandler}});
よくあることですが、イベントオブジェクトそのものに本当に興味がないことがあります。イベントを発動する要素に関連する何らかのデータがあり、そのデータを代わりに渡したいことがよくあります。
3つのボタンをもつカウンターのアプリケーションを考えてみましょう。ボタンはそれぞれカウンターを1、2、3だけ増やします。どのボタンが押されたのか本当に気にしていないとします。代わりに、クリックされたボタンに関連する数値に興味があります。イベントリスナーモジュールは名前つきのイベントプロパティで配列を提供することでそれを表現することを可能にします。配列の第1要素はイベントが起きたときに起動する関数が配列に含まれ、第2引数は関数に渡すデータです。
function clickHandler(number) { console.log('button ' + number + ' was clicked!'); }
h('div', [
h('a', {on: {click: [clickHandler, 1]}}),
h('a', {on: {click: [clickHandler, 2]}}),
h('a', {on: {click: [clickHandler, 3]}}),
]);
Snabbdom はレンダラーのあいだでイベントハンドラーをとりかえることを可能にします。DOM にアタッチされたイベントハンドラーに実際にタッチしなくてもこのことは起こります。
しかしながら、複数の VNode のあいだでイベントハンドラーを共有するとき、注意することが必要であることを心にとどめておくべきです。DOM にイベントハンドラーを再びバインドを避けるためにこのモジュールが使うテクニックがあるからです。そして、一般的には、複数の VNode のあいだでデータを共有することは動くことは保証されません。モジュールがデータを変化させることが許可されているからです。
とりわけ、次のようなことをすべきではありません。
// 動きません
var sharedHandler = {
change: function(e){ console.log('you chose: ' + e.target.value); }
};
h('div', [
h('input', {props: {type: 'radio', name: 'test', value: '0'},
on: sharedHandler}),
h('input', {props: {type: 'radio', name: 'test', value: '1'},
on: sharedHandler}),
h('input', {props: {type: 'radio', name: 'test', value: '2'},
on: sharedHandler})
]);
このようなケースの場合、配列をベースとしたハンドラーを代わりに使うことができます (下記を参照)。代わりに、それぞれのノードに独自の値が渡されることをご確認ください。
// 動きます
var sharedHandler = function(e){ console.log('you chose: ' + e.target.value); };
h('div', [
h('input', {props: {type: 'radio', name: 'test', value: '0'},
on: {change: sharedHandler}}),
h('input', {props: {type: 'radio', name: 'test', value: '1'},
on: {change: sharedHandler}}),
h('input', {props: {type: 'radio', name: 'test', value: '2'},
on: {change: sharedHandler}})
]);
ヘルパー
SVG
SVG は仮想ノードに対して h
関数を使うことでそのまま動きます。SVG 要素は適切な名前空間と一緒に自動的につくられます。
var vnode = h('div', [
h('svg', {attrs: {width: 100, height: 100}}, [
h('circle', {attrs: {cx: 50, cy: 50, r: 40, stroke: 'green', 'stroke-width': 4, fill: 'yellow'}})
])
]);
SVG の例と SVG のカルーセルの例もご覧ください。
thunk
thunk 関数は thunk を識別するための名前を引数にとります。thunk とは vnode と可変な状態パラメーターを返します。起動したとき、render 関数は状態を受け取ります。
thunk(uniqueName, renderFn, [stateAguments])
thunk はイミュータブルなデータを扱うために使うことのできる最適化戦略です。
数値をもとに仮想ノードをつくるためのシンプルな関数を考えてみましょう。
function numberView(n) {
return h('div', 'Number is: ' + n);
}
ビューは n にのみ依存します。このことが意味するのは n が変更されない場合、仮想 DOM ノードをつくり古い vnode に対してパッチを適用することが無駄であることを意味します。オーバーヘッドを避けるために、ヘルパー関数の thunk を使うことができます。
function render(state) {
return thunk('num', numberView, state.number);
}
numberView 関数を実際に起動する代わりに、このコードは仮想ツリーでダミーの vnode を起きます。When Snabbdom が以前の vnode に対してダミーの vnode に対してパッチを適用し、n の値を比較します。n が変化しない場合、古い vnode が使われます。これによって数値のビューの再生成および差分処理の両方が回避されます。
ここでのビュー関数は単なる例です。実際には、生成に大きな計算時間のかかる複雑なビューをレンダリングする場合にのみ thunk は関係します。