「Build your own React」のメモ
Build your own React
「Build your own React」という自分で小さなReactを作る記事があります。 以前から読んでみたかったので、重い腰を上げて全部読んだり、手を動かしてみました。
内容としては、Step by Stepでコードと実装の説明があり、最終的には以下のようなコードが動くようになります。
jsxは、自作のDidact.createElement
に変換し、Didact.render
関数を実行しています。
/** @jsx Didact.createElement */ function Counter() { const [state, setState] = Didact.useState(1); return ( <h1 onClick={() => setState((c) => c + 1)} style="user-select: none"> Count: {state} </h1> ); } const element = <Counter />; const container = document.getElementById("root"); Didact.render(element, container);
Step0からStep8まであります。
各Stepのメモ
各Stepの簡単なメモです。
Step Zero: Review
- JSXからcreateElementへの変換について
- typeとpropsプロパティを持つオブジェクトを返す(他にプロパティあるけど)
- type: tagName、document.createElementに渡すタグ名。
- props: childrenや属性を持つオブジェクト
- Reactを使わず、DOM APIを使った場合のコードに置き換える例を紹介
- ここはまだ事前知識の説明
Step I: The createElement
Function
- createElement関数を作成する
- propsやtypeを持つオブジェクトを作成するだけ
- type, props, childrenを受け取って、オブジェクトを返す
Step II: The render
Function
Step III: Concurrent Mode
- Step2で実装したコードは、ツリーに対して再帰的にrender関数を同期的に呼び出していたので、ツリーが大きいとメインスレッドを長時間ブロックしてしまう
- 作業を小さな単位に分割して、1つの作業が終わり、他に優先してやるべきことがあれば、レンダリングを中断するように実装する
- このStepではこの準備として、requestIdleCallbackを利用したループを作成する
- ブラウザがIdle状態の時に、小さな単位の作業を行う
- ReactはrequestIdleCallbackを使わずに、scheduler packageを実装している
- このStepではこの準備として、requestIdleCallbackを利用したループを作成する
Step IV: Fibers
- 作業の単位を整理するために、Fiberツリーというデータ構造が必要
- 各要素ごとに1つのFiberを持ち、各Fiberが作業の単位となる
- Fiberツリーは、次の作業を簡単に見つけやすいようになっている
- Step3で用意したrequestIdleCallbackのループにそって、要素のレンダリングといった作業を行うperformUnitOfWork関数を実装する
- 以下の処理を行う
- 要素をDOMに追加する
- 要素の子のFiberを作成する
- 次の作業を選択する
- 以下の処理を行う
Step V: Render and Commit Phases
- Step4までの実装だと、ツリー全体のレンダリングが完了する前に、ブラウザが作業を中断する可能性がある
- こうなると、レンダリング途中の中途半端なUIをユーザーが見ることになる
- FiberツリーのルートをwipRootとして記録しておき、すべてのperformUnitOfWorkを終えたタイミングで、Fiberツリー全体をDOMにコミットするように、フェーズを分離する
Step VI: Reconciliation
- このStepで要素の追加だけではなく、更新や削除も行えるようにする
- render 関数で受け取った要素を、DOM にコミットした最後のFiberツリーと比較する
- コミット後、DOMにコミットしたFiberツリーへの参照を保持しておく
- 各Fiberにalternateとして、前回のコミットフェーズでDOMにコミットしたFiberへのリンクも保持しておく
- 比較して
- 要素のtypeが同じなら、DOMノードはそのままで、新しいpropsで更新する
- typeが違くて、新しい要素がある場合は、DOMノードを追加する
- typeが違くて、古いFIberがある場合は、古いノードを削除する
- Fiberに、どのように変更するかを表すeffectTagを追加して、コミット時にeffectTagに応じたDOM操作をする
Step VII: Function Components
- 関数コンポーネントをサポートする
- 関数コンポーネントのFiberはDOMノードを持たない点やchildrenは関数を実行しないと取得できない点がこれまでと違う
- Fiberのtypeが関数の場合は、まず関数を実行してchildrenを受け取る(関数コンポーネントでreturnしてるオブジェクト)
- その後、DOMへのコミット時にDOMノードを持たないケースに対応する
- 親のDOMノードを見つける際は、DOMノードを持つFiberが見つかるまでツリーをさかのぼる必要がある
- また、要素の削除時にDOMを持つchildrenまでさかのぼる必要がある
Step VIII: Hooks
- useStateを実装する
- 同じコンポーネントで複数回、useStateを呼び出せるようにHooksを保持する配列や処理中のHooksのIndexといった変数を用意する
- stateやsetStateを実装する
- stateは、既にhookを持っているなら、そのhookのstateを返し、持ってないなら初期値を返す
- setStateは、actionを受け取り、queueとして保持しておき、root要素から更新を行う
- 更新時にqueueに保持しているactionを実行して、stateが更新される
その他
- 全体として、Reactのコードと同じ変数名や関数名を使っているらしい
感想
Step0からStep3までは結構理解できましたが、Step4のFiberやStep6のReconciliationは難しくて、よくわかってない部分が多いです。 全体を2回くらい見たり、コードをデバッグして、なんとか理解を深めることができました。
これまで、仮想DOMやReactについて、なんとなくだった部分が少しだけ、解像度が上がった気がします。