Vue.jsで学ぶVirtual DOM
この記事はVue.js Advent Calendar 2016の7日目(代打投稿)です。
Vue.jsも2.0になってVirtual DOMが採用されたので、改めてVirtual DOMについてMVVMとの比較で考えてみます。
いきなり結論っぽい図ですが、これが分かればもうVirtual DOMは理解したといっても過言ではないかと。
- ObjectとDOMはそれぞれ独立した状態(値)を持っており、それを双方向に同期する。
- キー入力に起因するDOMの更新はDOMが自律的に行う。
- DOMはObjectの射影であり、DOMが自律的に更新することは無い。
- キー入力はイベントの形でObject側に通知され、Objectの値が変更されることによって、その射影という形でDOMが更新される。
ただ、かつての自分がそうだったように、この図だけでは特に「なぜ"1-way data flow"がこれだけモテはやされているのか?」が理解できないかもしれないので、実際にコードでそのメリットを感じてもらいたいと思います。
例として使うのは、前回Vuejs v2で無理やり2-wayディレクティブを再現してお茶を濁した3-stateチェックボックスです。
これを今回は「正しい」マイグレーション方法であるVirtual DOMを使ったコンポーネントで書き直してみます。
まずはVue.js v1のカスタムディレクティブ:MVVMで書いたコードを以下に再掲します。
function threeStateByClick(cb) { var val = true; if (cb.readonly) { // 本来はcb.indeterminate(値が保持されないのでreadonlyを代用) cb.readonly = cb.indeterminate = false; val = cb.checked = false; } else if (!cb.checked) { cb.readonly = cb.indeterminate = true; val = null; } return val; } function threeStateByVal(cb, val) { if (val === null) { cb.readonly = cb.indeterminate = true; cb.checked = false; } else { cb.readonly = cb.indeterminate = false; cb.checked = val; } } Vue.directive('three-state', { twoWay: true, bind: function() { this.handler = function() { var val = threeStateByClick(this.el); this.set(val); }.bind(this); this.el.addEventListener('click', this.handler); }, unbind: function() { this.el.removeEventListener('click', this.handler); }, update: function(val) { threeStateByVal(this.el, val); }, });
[htmlでの指定方法]
<input type="checkbox" v-three-state="cbVal" />
で、こっちがVue.js v2のコンポーネント:Virtual DOMで書き直したコードです。
Vue.component('checkbox3', { props: [ 'value' ], render: function(createElement) { return createElement('input', { attrs: { type: 'checkbox' }, domProps: { checked: this.value, indeterminate: (this.value === null) }, on: { click: this.onClick } }); }, methods: { onClick: function(event) { // 次の値の決定(false->true->null->false...) switch(this.value) { case null: this.value = false; break; case true: this.value = null; break; case false: this.value = true; break; } this.$emit('input', this.value); } } });
[htmlでの指定方法]
<checkbox3 v-model="cbVal"></checkbox3>
methodsは分かるものとして、propsとrenderはそれぞれプロパティと描画(レンダリング)の定義です。
props: [ 'value' ]
と書いておけば、Objectとの紐づけ(data bind)が組込みのv-modelディレクティブで可能となります。
コード量が減っていることが分かるかと思います。この差はどこから来るのかというと、ひとえに
- DOMを操作するためのコードが半分(vm→v方向のみ)で済むから
です。
冒頭のMVVMの図の左上を再度チェックしてほしいのですが、そこには「更新」という説明とともにループ状の矢印線が書いてあります。
これが単なるinputタグであれば、DOMの状態更新は何の追加コード無しに勝手に実行されます。
ですが、今回の3-stateチェックボックスのように「indeterminateプロパティはJavascriptのコードからのみ変更可能」というような場合は、表示状態の更新のためのコードが必要となります。
threeStateByClick関数がそれに該当し、このメソッドではclickされたことによる次の値の決定と合わせて表示を更新しています。
一方、Virtual DOMでの場合はvm→v方向の状態設定のみで済み、またその方法も"domProps"でindeterminateプロパティに直接必要な値を設定するだけです。
MVVMのときのように「DOM単体でも現在の自身の状態を把握する必要があるが、indeterminateプロパティは値が保持されないのでreadonlyにもセットしておく」などという小細工は必要ありません。
Virtual DOMにおけるDOMはそれに紐づけされたObjectの射影であり、それゆえDOM自身は状態を把握しなくてよいからです(更新が必要な時は常にrender関数が呼び出されてDOMが書き直されるため)。
「これは例が良くない:indeterminateプロパティという特殊事情がそうさせているだけでは?」と思うかもしれませんが、例えば良くあるUIエフェクトの
- inputタブへの値の入力有無でlabelの色や表示位置が変わる
などといった場合でも同様のことが言えます。
MVVMではUI入力によるDOMの更新はv→vm方向のデータ同期とは別に実行されることが前提なので、この例でのlabel制御はinputタグのイベントハンドラ内に頑張って実装するしかありませんし、vm→v方向のデータ同期時のlabel制御コードもやっぱり必要です(でないと、Ajax等で値を取ってきたときにlabel表示が変わらないというバグ持ちになる)。
一方、Virtual DOMではこの場合においてもobjectの値に対応したlabelの状態を設定するコードをrender関数に書けば終わりです。
Virtual DOMは「本当のDOMがいつ生成・消滅されるかわからない」ので既存のコード資産、特にjQueryを使った各種ライブラリとの併用が非常に困難(不可能ではない)という問題もありますが、「更新処理がシンプルになる」ことが「DOMとの状態のズレが発生し得ない」ことと合わせて最大のメリットなのかな、というのが今の自分の思うところです(「描画速度が速い」とかは二の次)。