今回はReact.jsのVirtualDOMの実装での工夫について書きたいと思います。
Version control for the DOM
React.jsのVirtualDOMの実態はJavaScriptのオブジェクトであり、rerenderする際に前後の状態を比較して最小限の変更だけを実際のDOMに反映させる仕組みになっています。
つまり、バージョン管理されていてdiffだけをpatchとして実際のDOMに適用する感じですね。
Level by level
単純にVirtualDOMのtreeを比較すると計算量が多くなってしまうので、React.jsでは計算量を減らすための工夫がされています。
その1つがVirtualDOM treeの同階層同士でしか比較しないということです。WebアプリケーションのDOM構造で異なる階層に要素が移動するケースは珍しいという理由でこのようになっていて、これによってアルゴリズムの複雑性が削減されています。
<div><Header title="foo" /></div>
↓
<div><Header title="bar" /></div>
はよくあるけど
<div><Header title="foo" /></div>
↓
<div><div><Header title="foo" /></div></div>
みたいなことはあまりないので、この場合は、Header Componentは破棄されて再度作成される。
List
例えばあるリストの真ん中に要素を追加した時に、それぞれの要素を適切にマッピングして真ん中に要素が追加されたことだけを反映させるのはなかなか難しいです。
React.jsではkey属性を与えることで要素のマッピングを教えることが出来ます。
<ul>
{this.props.list.map(element => <li key={element.id}>{element.body</li>)}
</ul>
keyはそのデータのIdentifyを指定するべきです。indexとかにしても...
[
{ id: 1, name: "foo" },
{ id: 3, name: "bar" },
{ id: 5, name: "baz" }
]
↓
[
{ id: 1, name: "foo" },
{ id: 2, name: "foofoo" },
{ id: 3, name: "bar" },
{ id: 5, name: "baz" }
]
のようなデータ構造があるときは、idをkeyとして指定しておくといいです。
Components
React.jsは同じComponent同士に対してしかdiffアルゴリズムを適用しないので、すべてをdiv要素にするのではなくて独自のComponentを定義して使うことで無駄なdiffの計算を減らすことが出来ます。
<div>header1</div> -> <div>header2</div>
だと中の要素を比較しますが、<Header1 /> -> <Header2 />
だとHeader1を削除してHeader2を追加となります。逆にいうとHeader1とHeader2の違いが些細な場合は、同じComponentにして差分だけを適用した方がいいということになります。
Rendering
Batching
React.jsではsetStateするとそのComponentはdirtyであるとチェックされて、イベントループ毎にdirtyなComponentをrerenderすることで実際のDOMへの反映自体を最低限の回数にしています。
この工夫はVue.jsなど他のフレームワークやライブラリでも見られます。
Subtree rendering
また、setStateされたときは子のComponentもrerenderされます。なのでTopレベルのComponentでsetStateすると全てのComponentがrerenderされます。でも実際のDOMへは変更された箇所しか適用されないのでパフォーマンス的にはそんなに問題にならないです。VirtualDOMのdiffの計算がありますが実際のDOM操作に比べるとコストが低くVirtualDOMのなせる技です。
それによって、Topレベルの要素にデータをまとめてもたせておいてそれをsetStateで更新して都度アプリケーション全体をrerenderする構造が可能になってアプリケーションの構造を単純化することが出来るメリットもあります。
Selective sub-tree rendering
さらにパフォーマンスが求められるような場合には、boolean shouldComponentUpdate(object nextProps, object nextState)
を実装することでパフォーマンスを向上させることが出来ます。ここでfalseを返すとそのComponentとそれより下位のComponentがrerenderされなくなります。
ここでの比較を単純にするために、StateやPropをimmutableなdata構造にするというアプローチもあります。
immutable data
その辺りのソリューションとして、immutableなdataを持つClojureScriptでのReact.js実装であるOmや、Facebookが作っているimmutableなデータ操作を可能にするimmutable.jsがあったりします。
またReact.jsにもReact.addons.update
というAddonがあるのでそれを使うことも可能です。
immutableなデータ構造にすることによってundoなんかの実装を作りやすくなったりします。
最後少し話がそれましたが、そんな感じでDOM操作を最低限にしたりdiffの計算量をO(n^3)からO(n)にするためにReact.jsが工夫しているという話でした。