virtual-dom のイベント処理

仮想 DOM を扱うための JavaScript 用ライブラリ virtual-dom では ev-xxx (例. ev-click) でイベント処理を扱えるようになっていますが、実際に機能させるには dom-delegator が必要なようです。

virtual-dom は bower install が可能で Web ブラウザ上で直接使えるファイル (dist/virtual-dom.js) を用意してくれていますが、今のところ dom-delegator はそうなっていません。(dom-delegator は npm install する)

名称 bower install Web ブラウザで直接使用
virtual-dom
dom-delegator × ×

そこで、今回は下記ツールを用いて virtual-dom で ev-click が機能するようにしてみます。

事前準備

Gulp・Bower・Browserify をインストールします。

インストール例
> npm install -g gulp bower browserify

virtual-dom のイベント処理サンプル

virtual-dom で ev-click を実施するサンプルを作成します。

ソースは http://github.com/fits/try_samples/tree/master/blog/20160107/

(1) virtual-dom インストール

virtual-dom を Bower でインストールします。

bower init と virtual-dom インストール例
> bower init
・・・

> bower install virtual-dom --save
・・・

(2) dom-delegator インストール

dom-delegator は npm でインストールします。 (dom-delegator は Bower でインストールできないため ※)

※ dom-delegator の git リポジトリを bower install する事は可能ですが、
   その場合は依存ライブラリを手動でインストールする事になります
npm init と dom-delegator インストール例
> npm init
・・・

> npm install dom-delegator --save
・・・

(3) gulp-flatten インストール

Gulp でファイルをコピーする際にディレクトリ構成をフラット化するため gulp-flatten パッケージをインストールしておきます。

gulp-flatten インストール例
> npm install gulp-flatten --save-dev

Gulp で以下のように bower_components/virtual-dom/dist/virtual-dom.js を js ディレクトリへコピーすると、js/virtual-dom/dist/virtual-dom.js が作られます。

Gulp によるコピー例
gulp.task('js-copy', () => {
   gulp.src('bower_components/*/dist/*.js')
        .pipe(gulp.dest('js'));
});

今回は、js/virtual-dom/dist/virtual-dom.js では無く js/virtual-dom.js へコピーしたかったので gulp-flatten を使いました。

(4) Gulp ビルド定義作成と実行

以下のような処理を行う gulpfile.js を作成します

  • (a) bower_components/virtual-dom/dist/virtual-dom.js を js/virtual-dom.js へコピー
  • (b) dom-delegator を Browserify で処理して js/dom-delegator.js へ出力 (Web ブラウザ上で DOMDelegator という名称で扱えるように standalone も設定)
gulpfile.js
var fs = require('fs');

var gulp = require('gulp');
var browserify = require('browserify');

var flatten = require('gulp-flatten');

// (a)
gulp.task('js-copy', () => {
    gulp.src('bower_components/*/dist/*.js')
        .pipe(flatten()) // gulp-flatten でディレクトリをフラット化
        .pipe(gulp.dest('js'));
});

// (b)
gulp.task('browserify', () => {
    browserify({
        require: 'dom-delegator',
        standalone: 'DOMDelegator'
    }).bundle().pipe(fs.createWriteStream('js/dom-delegator.js'));

});

gulp.task('default', ['js-copy', 'browserify']);

js ディレクトリを手動で作成した後、gulp コマンドを実行します。

gulp 実行例
> mkdir js
> gulp

[00:19:17] Using gulpfile ・・・\virtual-dom\gulpfile.js
[00:19:17] Starting 'js-copy'...
[00:19:17] Finished 'js-copy' after 15 ms
[00:19:17] Starting 'browserify'...
[00:19:17] Finished 'browserify' after 17 ms
[00:19:17] Starting 'default'...
[00:19:17] Finished 'default' after 6.16 μs

これで下記のファイルが作成されます。

  • js/virtual-dom.js
  • js/dom-delegator.js

(5) virtual-dom の ev-click サンプル作成と実行

Gulp で生成した js ファイルを使い virtual-dom で ev-click を扱う単純なサンプルを作成し動作確認してみます。

virtual-dom は概ね以下のようにして使います。

  • (1) virtualDom.diff 関数で 2つの VNode (VirtualNode) の差分を VPatch (VirtualPatch) として抽出
  • (2) virtualDom.patch 関数で VPatch の内容を実際の DOM へ反映
index.html
<!DOCTYPE html>
<html>
<script src="js/virtual-dom.js"></script>
<script src="js/dom-delegator.js"></script>
<body>
    <h1>virtual-dom click sample</h1>

    <div id="ct"></div>

    <script>
        (function() {
            // ev-xxx イベントの有効化 (dom-delegator)
            new DOMDelegator();

            var h = virtualDom.h;
            var create = virtualDom.create;
            var diff = virtualDom.diff;
            var patch = virtualDom.patch;

            var index = 1;
            var tree = h();

            var render = function(v) {
                // 仮想 DOM の作成
                return h('div', [
                    h('button', 
                        {
                            // click イベントの設定
                            'ev-click': function(ev) { update(index++) }
                        },
                        ['countUp']
                    ),
                    h('br'),
                    'counter: ' + String(v)
                ]);
            }

            var update = function(v) {
                var newTree = render(v);

                // tree と newTree の差分を実際の DOM へ反映
                patch(document.getElementById('ct'), diff(tree, newTree));

                tree = newTree;
            };

            update(index++);
        })();

    </script>
</body>
</html>

Web ブラウザで実行し countUp ボタンを押下すると counter の数値がカウントアップする事を確認できました。

Web ブラウザ表示例

f:id:fits:20160107023648p:plain

virtual-dom イベント処理の仕組み

最後に、virtual-dom と dom-delegator がどのようにイベント処理を行っているのか簡単に書いておきます。

まず、VNode の作成時に ev- で始まるプロパティは EvHook へ変換されます。(virtual-hyperscript/index.js の transformProperties)

patch 関数で実際の DOM へ反映する際に、対象の DOM ノードへ特殊なプロパティ __EV_STORE_KEY@7 ※ を追加して EvHook の内容 (イベント名とイベント処理関数) を設定します。 (ev-store の機能)

※ プロパティ名は ev-store の index.js で定義されており、
   ev-store 7.0.0 では __EV_STORE_KEY@7 となっています
__EV_STORE_KEY@7 が設定されている DOM ノード例

f:id:fits:20160107023717p:plain

dom-delegator は "blur", "change", "click" 等の多数のイベントを document.documentElement で listen するように処理し、イベント発生時に対象 DOM ノードの __EV_STORE_KEY@7 プロパティの内容をチェックして、該当するイベントが設定されていればその処理関数を実行します。

つまり、上記のようなイベント処理を自前で行えば dom-delegator を使わなくても ev-click を機能させる事が可能です。

index2.html (dom-delegator を使わずに ev-click を有効化する例)
<!DOCTYPE html>
<html>
<script src="js/virtual-dom.js"></script>
<body>
    <h1>virtual-dom click sample2</h1>

    <div id="ct"></div>

    <script>
        (function() {
            // dom-delegator の代用処理
            document.addEventListener('click', function(ev) {
                var store = ev.target['__EV_STORE_KEY@7'];

                if (store && store[ev.type]) {
                    // 該当するイベント処理関数の実行
                    store[ev.type](ev);
                }
            }, true);

            var h = virtualDom.h;
            ・・・
            var render = function(v) {
                return h('div', [
                    h('button', 
                        {
                            'ev-click': function(ev) { update(index++) }
                        },
                        ['countUp']
                    ),
                    h('br'),
                    'counter: ' + String(v)
                ]);
            }
            ・・・
        })();

    </script>
</body>
</html>