Vue.jsで学ぶVirtual DOM

この記事はVue.js Advent Calendar 2016の7日目(代打投稿)です。

Vue.jsも2.0になってVirtual DOMが採用されたので、改めてVirtual DOMについてMVVMとの比較で考えてみます。
いきなり結論っぽい図ですが、これが分かればもうVirtual DOMは理解したといっても過言ではないかと。

MVVM(2-way data binding)

  • ObjectとDOMはそれぞれ独立した状態(値)を持っており、それを双方向に同期する。
  • キー入力に起因するDOMの更新はDOMが自律的に行う。

Virtual DOM(1-way data flow)

  • 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との状態のズレが発生し得ない」ことと合わせて最大のメリットなのかな、というのが今の自分の思うところです(「描画速度が速い」とかは二の次)。