Virtual DOMについて改めて調べる


概要

社内勉強会のメモ。

Reactに仮想DOM - Virtual DOM - が使われていることは知っているし、「仮想的なDOMツリーがあって、DOMツリーやDOM要素に変更があった場合に差分を見て変更があった部分だけ実際に書き換える仕組み」だということは理解してるけど、もう少し深掘りしてみる。


そもそもDOMってなんですか

よく使うけど、使ってる人数に対して何の略だか知ってる人数少なそう。
なんか紫で太いイメージがありますが。

DOM の紹介 - MDN

Document Object Model (DOM) は HTML や XML 文書のためのプログラミングインターフェイスです。

つまりブラウザで表示する文書をプログラムから扱うためのモデルのことですね。

仕様は以下。

他にもDOM自体については色々みんな書いてるのでこれくらいで。


Reactのドキュメント

仮想 DOM と内部処理 - React FAQ

仮想DOMは、差分検出処理をするための概念。

差分検出処理 - React Docs

  • 異なる型のDOM要素
    • 型っていうのはここではタグだと解釈して良さそう。
    • つまり異なるタグのDOM要素に変わる場合、その要素及びその子要素は全て置換される。
  • 同じ型のDOM要素
    • 同じタグのDOM要素に変わる場合、変更のある属性やスタイルだけが変更される。
    • これ子要素はどうなる?[要確認]
  • 同じ型のコンポーネント要素
    • 同じコンポーネントの場合はインスタンスは破棄されず、そのコンポーネントのReactStateは保持され、変更のあるpropsを更新していく。

子要素の再帰的な処理

ここは結構重要。Reactの keyの話。

Reactは子要素を比較する場合、単純に上から順に一つずつ比較していく。
なので、末尾に要素を追加する場合は1つだけの修正になるが、先頭に追加したりすると全部が一つずつずれるため、全ての要素に変更が入る。


<div>
  <div>Hoge</div>
  <div>Fuga</div>
</div>

<!-- 末尾に追加 -->
<div>
  <div>Hoge</div>
  <div>Fuga</div>
  <div>Piyo</div>
</div>

は、変更が一つだけ(Piyoだけ追加)で問題なし。


<div>
  <div>Hoge</div>
  <div>Fuga</div>
</div>

<!-- 先頭に追加 -->
<div>
  <div>Piyo</div>
  <div>Hoge</div>
  <div>Fuga</div>
</div>

は、一個も同じじゃないため子要素全てを置換してしまう。


本来であれば先頭に追加したものも先頭だけの修正にしたいので、これを回避するためにkey属性を使用する。
Reactは子要素を比較する際に、子要素がkey属性を持つ場合はkey属性の値が同じ要素を比較する。

keyって何も考えないと「ループで要素を作るときにwarningが出るからつける」みたいな感じになるけど、別にループで作る要素に限らずつけて良いってことだなー。
というか、要素の順番が変わる可能性がある場合は必ずつけた方が良い。


で、この文章を読む限り「子要素を比較する際に」keyを比較するため、親要素が違う場合はkeyが同じでも問題ない(関係ない)ということになる。
つまり、keyをプロジェクト全体で一意にする必要はない。


ただし、ループの中で配列のインデックスを渡すのはあまり良くなくて、配列の順番が変わった場合に、要素が同じでもkeyが変わってしまうので、意味がなくなる。

しかも、keyが変わるとコンポーネントのインスタンスは破棄されてしまう([要確認])ため、keyの付け方を間違えるとむしろ効率の悪いレンダリング処理になってしまう可能性がある。


上記のようなことを考慮して、とにかく変更の少ないDOMツリーを意識することが重要。


Reactドキュメント以外の情報

基本的には上記の差分検出処理 (reconciliation)をするための仕組みを仮想DOMと呼ぶけど、仮想DOMの魅力をより理解するためにはまだまだこれだけでは情報が足りない。

恐らくネット上で仮想DOMを語ろうとするとこの記事を避けては通れない。

なぜ仮想DOMという概念が俺達の魂を震えさせるのか

こちらの記事からいくつかわかりやすい言葉をピックアップします。


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

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

上述の差分検出処理の実際の実装の話。


ユーザーは、key属性を指定する以外を除いて、内部で行われる操作を知る必要はありません。基本的に、ユーザーが取りうる行動は、「常に完成品の仮想DOMをPushし続ける」ということになります。

Reactが隠蔽してくれるおかげで、僕らはサーバでHTMLをレンダリングしていた時と同じように、常に完成品のDOMツリーを(Reactに向けて)提示すれば良い、実際のレンダリングは最適化してReactが行ってくれる、という話。

なぜ今Fluxのような話が出てきたか
Virtual DOMによって「パフォーマンス面で問題を出さずに」常にゼロから状態を構築する、ということが可能になったからです。

なるほどー


shouldComponentUpdate

shouldComponentUpdate() - React Docs

このメソッドはパフォーマンスの最適化としてのみ存在します。バグを引き起こす可能性があるので、レンダーを「抑止する」ためにそれを使用しないでください。shouldComponentUpdate() を書く代わりに、組み込みの PureComponent を使用することを検討してください。PureComponent は props と state を浅く比較し、必要なアップデートをスキップする可能性を減らします。


パフォーマンス最適化 - React Docs

読むべきだけど割と長い。


どうすれば shouldComponentUpdate を実装できますか? - React Docs

関数コンポーネントを React.memo でラップして props を浅く比較するようにしてください。

const Button = React.memo((props) => {
  // your component
});

このアーキテクチャ、副作用のないImmutableオブジェクト(ここでは仮想DOM)を時系列ストリームで表現する、と言い変えると、関数型言語とFunctional Reactive Programmingと相性がよく、Virtual DOMはFRPを行うためのパーツとして振る舞えるようになります。

Reactが関数型言語っぽくなろうとしている(Hooksへの流れとか)のは、この辺に起因しているんだなー。
関数型言語とFRPに馴染みはないけども、その辺も理解していきたいなー。


その他参考になる記事


このメモ書いてから思ったけど、そもそもDOMとは?から入ってる仮想DOM説明の記事めっちゃ多くて、予期せずN番煎じ感がすごい形式になっていた。南無三。