@ledsun blog

Hのキーがhellで、Sのキーがslaveだ、と彼は思った。そしてYのキーがyouだ。

DOM更新アルゴリズムを実装しました

github.com

動機

virtual-domの良さ

Reactに代表されるようにGitHub - Matt-Esch/virtual-dom: A Virtual DOM and diffing algorithmを使うと、デザイン変更時に、JavaScriptのロジックを考えずに、HTMLとCSSを考えるだけよくなることがわかっています。

一方でvirtual-domはGitHub - hyperhype/hyperscript: Create HyperText with JavaScript.形式のAST(抽象構文木)を自分で作る必要があります。そこでReactではIntroducing JSX - Reactという新しい記法を作って、HTMLライクに記述できる様にしました。

JSXの悪さ

JSXには2つ問題があります。

  1. HTMLではない
  2. Babel · The compiler for writing next generation JavaScriptでJSXからJavaScriptに変換する必要がある

最初から、Reactを使うのであれば問題ありません。 既存のWebアプリケーションに部分的に適用する場合は、BabelとJSXの投入は大きな変更です。 JSXは、HTMLのようでHTMLではありません。HTMLからJSXに置き換える作業が必要です。 ワークフロー上、Babelを使っていなかった場合は、新規にBabelを使用する必要があります。

HTMLを入力とする

そこで、HTMLを入力としてDOMを更新する関数を作成しました。 Handlebars.js: Minimal Templating on Steroidsの様なHTMLを生成するテンプレートエンジンを使っている場合は、生成したHTMLをそのまま入力値として使えます。 テンプレートエンジンを使っていない場合でも、現在表示しているDOMを、Google Chromeの開発ツールを使ってHTML形式でコピーすることで、流用できます。 この場合は、動的に置き換えたい値をテンプレートリテラルやテンプレートエンジンを使って置き換える作業が必要です。

設計

大方針

  1. GitHub - inikulin/parse5: HTML parsing/serialization toolset for Node.js. WHATWG HTML Living Standard (aka HTML5)-compliant.を使ってHTMLをASTに変換
  2. ASTを辿ってDOMに反映

細かい知見

appendChildはツリーの帰り道で行う

DOMにNodeを追加すると、その度にNode追加後の表示要素の場所を再計算します。 Nodeを追加する際は、Nodeとその子Nodeを作成してから、Node.appendChild - Web API インターフェイス | MDNします。

ツリーを辿る順序では行きでNodeを作成し、帰りでNodeを追加します。 ロジックとしては、子Nodeの更新が終わった後に、appendChildします。

Nodeの型の変更

DOMは既存のNodeの型を別の型に変更できません。 例えばdivspanに変更できません。またElement(HTMLタグ)をTextNode(タグの中身)に変更すること、その逆もできません。

型が変わった時は、新しいNodeを作成し、Node.replaceChild - Web API インターフェイス | MDNを使って既存のNodeと入れ替えます。

boolean attributes

Nodeにはboolean attributesという属性があります。

例えば

  • disabled="disabled"
  • checked="checked"

です。 これらはDOM上の属性をelement.setAttribute - Web API インターフェイス | MDNで変更しても、ブラウザ上の見た目には反映されません。

Node.disabledなどの、個別のプロパティを併用する必要があります。

やっていないこと

リストへの挿入時の最適化はしていません。 liタグなどで作ったリストに、最後尾への追加でなく、途中への挿入をした場合、それ以降の要素は全て変更したとして扱います。

経緯

Ruby でつくるRubyを読んでASTの辿り方を学んだ

ledsun.hatenablog.com

わずか120行のソースコードを写経するだけで、ASTの扱い方が理解できる本です。

update-dom-tree基本戦略は、この本のASTの扱いと一緒です。 ASTを辿ると同時に評価し、DOMに反映します。

virtual-domは、差分検知と更新の二段階に分かれています。 変更を検知したNode以下を作り直し、replaceChildします。

Node.jsのスクレイピング情報を整理してHTMLのASTを得る方法を学んだ

qiita.com

スクレイピングのは

  1. HTMLの取得
  2. HTMLの評価

の2要素からなります。 このうちHTMLの評価ではHTMLのパースが必要です。 色々な方法があります。この時の知見からparse5を選択しました。

  1. pure JavaScript
  2. メジャーなライブラリで使われている
  3. 更新が活発

の3点を重視しました。

結果

デザイン変更の自由度の上がりっぷりは最高でした。

  1. HTMLでイメージに合ったUIを作る
  2. handlebarsを使ってデータとテンプレートに分ける
  3. update-dom-treeを使ってビュークラスを作る
  4. モデルクラスを作って、データ更新のAPIとイベントを作る

という手順でコンポーネントが作れます。 UIの

  • 表示項目
  • 表示イメージ
  • データの取得方法

が固まっている時に、サクサク作れて便利です。

実装にかかった時間は、初期実装とデバッグを合わせても丸1日程度でした。 それに比較すると、デザイン変更の自由度の上昇はかなり大きな利得です。

記録