toshi-toma blog

主にフロントエンド、作業ログあとは色々なメモ ✍️ 🍅

「Build your own React」のメモ

Build your own React

「Build your own React」という自分で小さなReactを作る記事があります。 以前から読んでみたかったので、重い腰を上げて全部読んだり、手を動かしてみました。

pomb.us

内容としては、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

  • render関数を作成する
  • Step1で作成されたオブジェクトをもとに、DOMをレンダリングする
  • あくまでDOMのレンダリングだけやるので、差分更新などはなし

Step III: Concurrent Mode

  • Step2で実装したコードは、ツリーに対して再帰的にrender関数を同期的に呼び出していたので、ツリーが大きいとメインスレッドを長時間ブロックしてしまう
  • 作業を小さな単位に分割して、1つの作業が終わり、他に優先してやるべきことがあれば、レンダリングを中断するように実装する
    • このStepではこの準備として、requestIdleCallbackを利用したループを作成する
      • ブラウザがIdle状態の時に、小さな単位の作業を行う
    • ReactはrequestIdleCallbackを使わずに、scheduler packageを実装している

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について、なんとなくだった部分が少しだけ、解像度が上がった気がします。