Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Reactの内部構造を知っておく (React Tokyo #6 - @calloc134)

Avatar for calloc134 calloc134
June 19, 2025
1.3k

Reactの内部構造を知っておく (React Tokyo #6 - @calloc134)

Avatar for calloc134

calloc134

June 19, 2025
Tweet

Transcript

  1. 自己紹介 かろっく @calloc134  Twitter: @calloc134  GitHub: @calloc134 

    Zenn: @calloc134 プロフィール U フロントエンドからインフラまで興味あり s 一言: 学生として、時間があるうちにしかできないようなことを今年はやりたい!
  2. 宣言的UIとは? 宣言的とは 「どのように達成するか (How)」ではなく 「何を達成するか(What)」に焦点を当てて処理を記述すること 例: キャンバスに四角形を描くタスクで比較 命令的アプローチ ペンを下ろす 右に線を引く

    下に線を引く 左に線を引く 上に線を引く 一つ一つの操作手順を細かく記述する 宣言的アプローチ 「キャンバスに四角形を描く」と宣言 何を達成するかのみを宣言する  命令的UI  宣言的UI (React)
  3. Reactの宣言的UI実装(1) Reactは仮想DOMと差分検出で効率的なレンダリングを実現 仮想DOM → 軽量な下描き用紙    実際のDOM → 本番のキャンバス 例:

    既にキャンバスにタコのイラストがあり、最後の足だけ手を挙げた状態にしたい 1 1 キャンバスをすべて消して再描画  ️ 非効率的 変更のない部分も含めて全体を再描画する 2 2 変更部分のみ手動更新  効率的だが手間 変更部分を識別して更新する必要がある 3 React方式 3 下描きで差分検出&更新 下描き 1 下描き2  差分検出  効率的かつ手間が少ない 差分のみを検出し自動的に更新 → React方式 React方式
  4. Reactの宣言的UI実装(2) Reactのアプローチ 下描きを用いて全部を再描画するが
 実際のキャンバスへの更新は差分のみを適用 * この部分をReactが担う 下描きには既にタコのイラストがある 1 新しく最後の足だけ手を挙げたタコを下描きする 2

    二つの下描きを比較して差分を取る* 3 実際のキャンバスに差分のみを適用* 4 簡単なまとめ このようにして開発者は詳細な手順を意識せずに
 「何を達成するか」のみを宣言的に記述できる → 開発者体験の大幅な向上 下描き1(現在の状態) 下描き2(更新後の状態)  差分検出 実際のキャンバス(差分のみ適用)
  5. Reactのレンダリングの流れ 4つのフェーズから構成され、効率的なUI更新を実現  トリガー フェーズ イベント発火  トリガー フェーズ レンダリングを開始する

    契機となるフェーズ イベント発火  スケジュール フェーズ レンダリングの優先度決定と 実行計画の管理 優先度判定  スケジュール フェーズ レンダリングの優先度決定と 実行計画の管理 優先度判定  レンダー フェーズ Fiber更新 中断可能  レンダー フェーズ 仮想DOMの更新と 差分検出を行うフェーズ Fiber更新 中断可能  コミット フェーズ DOM更新 中断不可  コミット フェーズ 実際のDOMに 差分を反映するフェーズ DOM更新 中断不可
  6. 仮想DOMの正体: Fiberノード (1) React内部で管理しているのは仮想DOMではなく「Fiberノード」と呼ばれるオブジェクトの木構造 (Fiberツリー) ※ 「仮想DOM」という言葉は厳密には不適切  Fiberノードはコンポーネントツリーを構成する基本要素 Fiberノードの主要プロパティ

    プロパティ名 説明 key コンポーネントの一意な識別子 再レンダリング時の要素マッチングに使用 tag コンポーネントのタイプ 関数コンポーネント、クラスコンポーネント、DOM要素など stateNode コンポーネントのインスタンス 実際に対応するDOMノードやクラスインスタンスなど
  7. Fiberノードの構造(2) Fiberノード同士の参照関係  child 第一子に当たる Fiber ノードへの参照  sibling 同じ親を持つ次の兄弟の

    Fiber ノードへの参照  return 親に当たる Fiber ノードへの参照  sibling を辿っていくことで 兄弟ノードを連結リストのようにたどることができるのもポイント A B1 B2 C child sibling child return return return
  8. Fiberノードの構造(3) alternate: 2つあるFiberノードのうち相対するもう一方のノードへの参照 (ない場合はnull) 1 Reactのレンダリングは 二つのFiberツリーを持ちながら形作られていく 2 もう片方のツリーで自身と同じ存在に対応するノード 3

    パラレルワールドの自分と繋がるための ポインタのようなもの 4 初回のレンダリングでは currentに対応するノードがないためalternateはnull Header (current) Nav (current) Main (current) Header (WIP) Nav (WIP) Main (WIP) alternate current Tree workInProgress Tree パラレルワールドのような2 つのツリーと、 対応するノード同士 を繋ぐ alternate 参照 パラレルワールドのような2 つのツリーと、 対応するノード同士 を繋ぐ alternate 参照
  9. FiberRootNodeの役割 初回レンダリングで が作成 FiberRootNode レンダリングに関連するFiberツリーはcurrent とworkInProgress の二つ current → 現在の表示状態を表すツリー

    workInProgress → 次のレンダリングで表示される状態を表すツリー FiberRootNodeはcurrent プロパティを参照 Fiberツリーの上に存在し、Fiberツリーを管理するためのノード どのような場合でも は一つだけ FiberRootNode Root FiberRootNode currentツリー ツリー workInProgress currentプロパティ
  10. 優先度: レーン 内部で32ビットのビットマスクとして優先度を表現 タスクをあたかも「車線(レーン)」を分けて走る車のように、優先度に応じた処理を実現  ユーザ入力の更新 → 高優先度  アニメーション

    → 中優先度  バックグラウンド更新 → 低優先度 「車線(レーン)」で表現される優先度 レーン 高優先度    中優先度   低優先度        二進数で表現するメリット → OR演算で優先度を組み合わせられる 入力処理: 0 0 1 0 0 ... アニメーション 0 1 0 0 0 ... OR演算 0 1 1 0 0 ...  OR演算で複数の優先度を持つタスクを表現でき、効率的な優先度マージが可能 0 0 1 0 0 ... 0 1 0 0 0 ... OR演算 0 1 1 0 0 ...
  11. トリガーフェーズ(1) レンダリングの開始を決定しタスクに登録 タスクを管理するキュー → を利用 優先度付きキュー 優先度付きキューの構造 末尾に新しい要素を追加することができる 優先度が高いものが優先的に取り出される タスクによって二種類のキューが存在

    taskQueue: すぐ実行されるタスクを管理するキュー timerQueue: 将来の実行を予定しているタスクを
 管理するキュー 優先度付きキューの図 1 2 3 7 優先度に基づいて自動的にソートされる構造 タスクキューの種類 taskQueue 優先度 高  期限 直近 すぐに実行が必要なタスク timerQueue 開始時刻 未来  スケジュール済 待機 スケジュール済 将来実行予定のタスク
  12. トリガーフェーズ(2) タスクオブジェクト作成 タスク開始予定時刻 タスクの開始予定時刻 タイムアウト値(最大遅延時間) タスクをどれだけ後回しにしていいかを示す値 タスクの期限切れ時刻 タスク開始予定時刻 + タイムアウト値

    その他の情報: タスクID、実行内容の関数など  タスクオブジェクトの生成 キューへの登録 1 2 3 timerQueue Task B Task D ... 将来実行タスク taskQueue Task A Task C ... すぐ実行タスク タスクオブジェクト 開始予定時刻、タイムアウト値、期限切れ時刻など 1 開始時刻の確認 タスクの開始時刻は現在時刻よりも未来か過去か? 2 未来の場合 timerQueueに登録 キー: 開始時刻 過去 の場合 t askQueueに登録 キー: 期限切れ時刻 3
  13. スケジュールフェーズ キューからタスクを取り出し実行判断 優先度に応じた効率的なタスク処理を行うための仕組み タスク取得と実行の流れ peek メソッドで一番上のタスクを取得 (キューから削除せず) 余裕がある & ホスト環境に処理を戻すべき

    → 中断 そうでない場合 → タスク実行 タスクが完了したら pop でキューから削除 完了していなかったら関数を差し替えて
 引き続きキューに残る  なぜスケジューリングが必要か? 長時間実行タスクによるUI更新のブロックを防ぐため 処理を細かく分割しブラウザに制御を戻す機会を作る タスクキューの操作 タスク実行フロー 1 peek() - タスク参照  2 判 断 - 実行 or 中断  中断  実行  po p() - 完了時のみ タスクA 優先度: 高 peek → タスクB 優先度: 中 タスクC 優先度: 低 tas kQu eu e
  14. レンダーフェーズ(2) beginWork 編 beginWork: 主な役割 関数コンポーネントの実行と差分検出を担当 関数コンポーネントの場合 DOM要素の場合 処理が簡素なため、本資料では詳細説明を省略 

    効率化のための最適化処理(bailout)などの詳細は省略しています コンポーネントを実行して要素を取得 子要素のフラグをマージ リコンシリエーション(差分検出処理) 関数コンポーネント実行フロー  関数コンポーネント実行  要素を返却 return ... ; <div> </div>    リコンシリエーション Fiberノードと結果要素の差分検出
  15. レンダーフェーズ(3)リコンシリエーション編 (1) リコンシリエーションは差分検出の要処理 現在のFiberノードと 要素を比較 レンダリング結果 配列要素の場合: まず位置ベースでマッチング すべてのキーと型が一致 →

    マッチング成功 マッチングに失敗した場合: Fiberノードが余る → → 余剰なFiberノードを削除するフラグ付与 要素の削除 要素が余る → → 新しいFiberノードを作成してフラグ付与 新規要素追加 どちらも余る → マッチング → 第二段階へ移行 失敗 位置ベースのマッチングの例 マッチング成功 現在のFiberノード A B C 新しい要素 A B C ✓ ✓ ✓ マッチング失敗例 要素削除 A A B B C なし 余剰Fiberノード削除 要素追加 A A B B なし C 新しいFiberノード作成 不一致 A A B D C C 第二段階へ移行
  16. レンダーフェーズ(4)リコンシリエーション編 (2) キーを利用したマッチング 位置ベースのマッチングが失敗した場合の第二段階処理 W 連想配列を作成:キーあり → key を使用、なし →

    index を使用 W マッチ成功時は alternate プロパティを利用して既存ノードを再利用 W 2段階マッチングにより効率的な差分検出を実現 1. 位置ベースのマッチング oldFiber[0] A ≠ newChild[0] D  インデックスでの比較に失敗 2. キーベースのマッチング 第二段階 key: "b" D キーで検索  "b" → B oldFiber B  newFiber D  キーでマッチング成功した要素は alternate を利用して再利用
  17. レンダーフェーズ(5) completeWork 編 completeWork: フラグ登録やDOM生成 DOM要素の場合の処理: 関数コンポーネントの場合は特に何もしない 最後に共通処理として親ノードに対するフラグのマージを行う 子ノードのフラグ情報を親に伝播させることで効率的な更新を実現 stateNode

    プロパティにDOMノードを登録 関連フラグを登録 1. stateNode にDOMノードを登録 Fiber Node type: 'div' stateNode DOM Node <div id="root"> Fiber Node type: 'div' stateNode DOM Node <div id="root"> 2. flags にフラグを登録 flags: 0 flags: 4 flags: 0  flags: 4 3. 親ノードにフラグをマージ Parent Node 0 Child A 4 Child B 8 12 フラグマージ後 Parent Node 0 Child A 4 Child B 8 12 フラグマージ後
  18. コミットフェーズ 実際のDOMに差分を反映 コミットフェーズはレンダー結果を実際のUIに適用する段階 Fiberノードに設定された を確認して反映 フラグ このフェーズは 中断不可 (一度始まると完了まで継続) DOM操作、参照の更新、

    ライフサイクルメソッドの呼び出しなど コミット完了時の重要な処理 すべての反映が終わった後、 FiberRootNode.currentを workInProgress に更新 → これにより次のレンダリングでは
 現在表示中の状態が「現状」として扱われる コミット前 FiberRootNodeのcurrentは古いツリーを指している FiberRootNode current currentツリー workInProgress ツリー  コミット後 FiberRootNode.current = workInProgress; FiberRootNode current 新しいcurrent ツリー
  19. まとめ  宣言的UI 「何を達成するか」に焦点を当てる  Fiberツリー比較 二つの下描きの差分を検出して更新  4つのフェーズ トリガー、スケジュール、レンダー、コミット

     深さ優先探索 DOM生成、差分検知まで効率的アルゴリズムを活用  差分だけ反映 コミットフェーズで実際のDOMに差分のみを反映
  20. おわりに・参考資料 React のソースコードは巨大であり、読まなくても当然アプリケーションを実装できるものです。 しかし、ソースコードを読むことでReact の挙動をより深く理解できることに間違いはありません。 今回の内容を更に詳しく知りたい方は、是非ブログ記事をご覧ください。 最後に Meta の React

    開発チームの皆様へ。 ソースコードの解釈が間違っていれば是非ご指摘ください。非常に助かります。 そして素晴らしいライブラリを提供していただき、ありがとうございます。これからも React の発展を楽しみにしています! 参考資料  https://deepwiki.com/facebook/react  https://zenn.dev/ktmouk/articles/68fefedb5fcbdc  https://jser.dev/series/react-source-code-walkthrough  https://incepter.github.io/how-react-works/