Virtual DOMや非同期なDOM操作のメリットと悩ましい点

ここしばらく耳にするようになった Virtual DOM ですが、実は拙作 pettanR というプロジェクトのために開発している UI フレームワーク、その DOM 操作レイヤーでも Real DOM 操作を非同期化していて Virtual DOM (っぽい)実装になっています。

ここで、“っぽい”としたのは、diff/patch という発想が無いことによります。

ご参考:VirtualDom - なぜ仮想DOMという概念が俺達の魂を震えさせるのか - Qiita

このHTMLの生成する元となるツリー構造は、生のDOM(HTMLのインスタンス)である必要はなく、DOMと1対1に対応する単純な構造体で表現し、それを仮想DOMと呼びます。

Virtual DOM実装といった場合、仮想DOMの構造体表現と、それを用いたdiff/patchアルゴリズムを指します。

ちなみに、pettanR の DOM 操作レイヤーの API は一見 jQuery によく似ていて、DOM 操作を非同期化した jQuery とでもいった風です。DOM 操作レイヤーの最初のコミットは 2013-07-19、このレイヤーを完全非同期化したのは 2014-02-16 からとなっています。しかし pettanR の紹介はまたに譲ることとします。

本記事では自身でも非同期な DOM をやってみた経験から、メリットとデメリットについて触れてみます。

非同期化した DOM のメリット

メリットとしてまず思いつくのが、DOM 操作の非同期化によって、最終的な値だけが Real DOM に渡される、という点です。

あまりに酷い例ですが、以下のように、通常であれば 2 度 HTML 要素のプロパティにアクセスしてしまう書き方をしても、実際に要素に触るのは一度、ということになります。

$('#msg').css('color','red');
if( !disabled ) $('#msg').css('color','green');

ところで以上のような変更を蓄えておいて適当なタイミングで一挙に patch、という最適化は当のブラウザ自身がやっていることです。これを js (フレームワークまたはライブラリ)レベルでも似たようなことをしている、と書くとちょっと微妙にきこえてきませんか?

DOM 操作を非同期化することには、patch を纏める他にもメリットがあります。

説明のために、次に、要素を生成し、属性などをセットし、文書ツリーに追加する、というよくある流れを示します。

生 javascript の例

var elm = document.createElement('div');
    elm.style.opacity='0.5';
    parent.appendChild(elm);

jQuery の例

var $elm = $('div')
    .css('opacity',0.5)
    .appendTo(parent);

jQuery を使った場合、opacity に対応していない IE8 以下に対しても filter を使った半透過を施してくれて素晴らしいですね。プログラマは filter 指定の詳しい書き方を忘れてしまっても構いません。

さて、ここでおやっと思った方いらっしゃいますね、そうです、実は先に示した順番では filter が意図したように働かないのです。IE の filter には要素が文書ツリーに追加された後に filter を指定しないと働かない、という欠陥があるためです。

プログラマは filter 指定の方法を忘れることはできますが、IE8 以下では先に文書ツリーに追加しそのあとから css (filter) をいじる、ということを依然として憶えていなくていけません。

$elm.appendTo(parent).css('opacity',0.5);

一方の 非同期 DOM では、Virtual DOM への変更が即座に Real DOM には反映されません。つまり、非同期 DOM フレームワークがブラウザを判定して最善のタイミングと方法を切り替えてオッケー、ということになります。

  1. どのタイミングで文書ツリーに追加するか?その際の documentFragument 使用の有無は?
  2. 属性やスタイル・イベントをセットするタイミングは?

IE8 以下では、先に文書ツリーに追加した後で css を更新します。一方他の多くのブラウザは、全ての属性を設定した後で文書ツリーに追加する、という最も自然なタイミングで操作するのが有利です。

また、新規に生成した要素に子要素がある場合ですが、通常は全ての子要素を追加した上で、最後に文書ツリーに追加するのが有利とされます。一方で古い IE には、要素の文書ツリーへの追加タイミングに絡むメモリリークもあります。

このようなことをプログラマーが覚えておくのは大変ですが、非同期化された DOM であれば、フレームワーク内で適切に処理してくれるので、プログラマーは忘れることができます。

非同期化された DOM の悩ましい点

メリットの一方で、非同期化することで遭遇した悩ましい点も紹介します。

$("<img>").appendTo(document.body).on("load",onLoad).attr("src","hoge.jpeg");

以上のようなコードの場合、jQuery では <img> の生成→ <body> に追加→ load イベントの監視→ src 属性の設定 という記述した順序で行われます。結果 onLoad 関数が(エラーの場合を除いて)確実に呼ばれることが期待できます。

一方の非同期 DOM ではいろいろあれで、Real DOM への操作の順序をフレームワーク利用者が細かくコントロールする手段を用意しないかもしれません。これがどう悩ましいかといいますと、プログラマーが以下のような順序を期待したとします。

var elm = document.createElement("img");
elm.setAttribute("src", "hoge.jpg");
elm.addEventListener("load", onLoad, false);
document.body.appendChild(elm);

この場合、hoge.jpg がキャッシュから読み込まれると src の設定で直ちにロード完了してしまい、onLoad が呼ばれないかもしれません。そしてプログラマーは(これはかなりひねくれてますが)onLoad が呼ばれないケースがあることを期待している奇抜な人かもしれません。

このように細かく操作の順番をコントロールする手段をフレームワーク利用者に提供するには、非同期 DOM は操作を単に蓄えるのではなく、操作の順番も控える必要があります。

これはさすがに割に合わないように思われ、pettanR では常にイベントの監視(addEventListener)が先としています。そして load イベント等の引き金になる src 等の属性は後のほうで行う、と決めうちにしています。

IE7 以下には、要素生成時に設定しておかないと無視される属性(form 部品の name や iframe の frameborder など)もあるので、生成時に適用する属性と最後に適用する属性(src 等通信イベントに絡むもの)を分けなくてはいけません。

// for IE7-
var elm = document.createElement('<iframe name="hoge" frameborder="0">');

最後に

“決めうちにしてしまう”こう書いてふと頭をよぎるのが、JSフレームワークはもうこりごりという記事です。

イベントの伝達方法やサポートされるタグの種類等、ブラウザ間における基礎的な要素に関してでさえ仕様が合致し ていなかったのです。

そのためフレームワークとは、欠点をカバーするだけではなく、ブラウザの動作仕様を規定する独自モデルを構築するものでした。

こういったブラウザの欠点は解消しつつあるように思います。

一方で最近の、Web ブラウザ内でネイティブの力を引き出していこう、という流れは、GPU という今まで隠蔽されていたものを表に引きずり出して、Web 開発に新たな困難さをもたらしているように思います…

これを丸く治めるためにはフレームワークはアリだと僕は思います。これについてはまた改めて書きたいと思います。ではでは。

そうそう、ブログのデザインをいじって今はとってもいい気分です。