勉強会を進めるにあたって、実装に必要な知識を整理します。
この章では React の差分検出処理(reconciliation)や、実際の React のリポジトリがどういった構成になっているのかについて焦点を合わせます。
差分検出処理とはいっても、何の差分を検出しているのでしょうか? React においては React 要素のツリーの差分を検出します。後でも触れますが React 要素のツリーとは以下のようなものです。
React.createElement(
"div",
null,
React.createElement("span", null),
React.createElement("span", null),
React.createElement("span", null)
);
JSX を JS に transform すると以上のような React 要素が得られます。
検出の行われ方は React の setState
関数が呼び出された際、その呼び出したコンポーネント以下の要素ツリーが再度描画を行う際の対象に入ります。このときルート要素(上の例では <div>
)が異なるもの(例えば <section>
など)に変更された場合は、元のルートが <div>
の要素ツリーは全て破棄されます。そして新しい <section>
をルートにした要素ツリーが構築されます。新たな要素ツリーを構築する時点で新しい DOM ノードが DOM に挿入されます。
では同じ要素で属性のみが更新された場合はどうなるでしょうか?このときもツリーを全て破棄するのは無駄です。React では例えば <div>
の className
だけが変更された場合、古いノードを保持したまま変更された className
の値だけを更新します。このような処理を子要素に向かって再帰的に繰り返していきます。
ちなみに今回の勉強会では実装を省きます(発展としては用意しています)が、この子要素の再帰的な差分検出処理の際に React は key
属性を利用しています。key
を利用することで子要素のどの位置に新たな要素が追加されても効率よくノードの更新を行うことができます( 参考 )。
つまり React の差分検出処理とは React の要素ツリーを比較・更新する再帰的な処理 ということをとりあえず覚えておいてください。
React は差分検出処理に fiber というデータ構造を使用しています。データ構造については実装中にお話しするとして、React が fiber を使うようになった目的について、ここで共有したいと思います。
React は最近まで fiber とは異なったデータ構造で差分検出処理を行っていました。聞いたことがある方もいらっしゃるかもしれませんが、stack というものが差分検出に利用されていました。
この stack にはいくつかの問題点がありました。 docs の fiber についての項では、この問題点を解決する他に fiber を導入した目的が色々と挙げられているのですが、今回の実装に関わりがあるものとして「中断可能な作業を小分けに分割する機能」と「進行中の作業に優先順位を付けたり、再配置や再利用する機能」の2つを取り上げます。
実際に実装を始めると stack の問題点と遭遇することになるのでぜひこの2つを覚えておいてください。1つ前の概観でお話しした、差分検出は再帰的な処理ということも手がかりとなります。
以上についてより深く知りたい場合は冒頭でも挙げた記事以外に以下が参考になります。
- https://github.com/acdlite/react-fiber-architecture
- https://blog.ag-grid.com/inside-fiber-an-in-depth-overview-of-the-new-reconciliation-algorithm-in-react/
- https://qiita.com/mizchi/items/4d25bc26def1719d52e6
- https://qiita.com/koba04/items/fd6d9f24f9f6ce50e47e
続いて React のリポジトリの構成を見てみましょう。今回の実装リポジトリもある程度この構成を倣っています。
https://github.com/facebook/react は主に React の各機能を packages/
に集め、React を検証するための様々なアプリケーションのようなものを fixtures/
に集めた monorepo になっています。
packages/
を少し見てみましょう。様々なものがパッケージ化されています。
packages
├── react-dom
...
├── react-reconciler
...
├── react
...
├── shared
...
react
は hooks やlazy
、createContext
など馴染みのあるものが API として公開されています。react-dom
では render
や hydrate
の他 renderToString
などサーバーサイドで利用するような API も用意されています。
react-reconciler
は最初に説明した差分検出処理のエンジンの実装が入っています。このパッケージは react-dom
など、他のレンダラーから API を利用されるような形になっています。
shared
はパッケージ間で共有するための様々なモジュールが入っています。例えば https://github.com/facebook/react/blob/cae635054e17a6f107a39d328649137b83f25972/packages/shared/ReactFeatureFlags.js では、ビルドの出し分けや新たな reconciler を使用するかどうかなどのフラグが書かれています。また https://github.com/facebook/react/blob/26666427d6ed5cbc581e65e43608fa1acec3bcf8/packages/shared/ReactSharedInternals.js を見てみると React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
というプロパティがあります。素晴らしい名前ですね。
ちなみにこの名前のプロパティはドキュメント化されていないAPIと内部データ構造のため、自己責任で使ってくださいということらしいです。: https://reactjs.org/docs/faq-versioning.html#what-counts-as-a-breaking-change
今回 shared
は実装しませんが、React はこういう感じの monorepo なんだなという参考程度に取り上げました。
他にもパッケージは存在しますが、 React のリポジトリ構成がこれである程度わかったと思うので、いよいよ実装に入っていきます。
実装をはじめる前に へ続く