React の描画パイプラインにおいて、render 段階は最も重要なプロセスです。ここでは、DOM への反映を行う前に、アプリケーションの状態変化を検知し、必要な更新を計算する「diff 処理」が行われます。本稿では、Fiber アーキテクチャにおける render 段階の具体的な動作、特に Fiber ツリーの構築プロセスと beginWork の役割に焦点を当てて解説します。
render 段階の全体像と実行フロー
render 段階は、performSyncWorkOnRoot の呼び出しから始まり、finishSyncRender によって完了します。この間、大量の beginWork と completeWork が呼び出され、ツリーの走査と更新作業が行われます。
React 15 までの協調処理(Reconciliation)は再帰呼び出しによって実装されていましたが、Fiber アーキテクチャではイテレーション(繰り返し処理)によって模拟されています。しかし、ReactDOM.render による同期モードでは、結果として深度優先探索(DFS)と同様の順序でノードが処理されます。
beginWork は新しい Fiber ノードの作成や更新判定を担当し、completeWork は Fiber ノードを実際の DOM ノードへ変換する準備を行います。この仕組みを理解するには、Fiber ツリーがどのように構築されるかを追う必要があります。
workInProgress ノードの生成と二重バッファ
render 段階の入口である renderRootSync の内部では、スタック環境をリセットする prepareFreshStack が実行され、その中で createWorkInProgress が呼び出されます。この関数は、現在のツリー(current ツリー)から作業用のツリー(workInProgress ツリー)を作成する役割を担います。
主要なロジックは以下の通り簡略化できます。
function cloneFiberNode(currentFiber, newProps) {
// 既存の alternate ノードがあればそれを再利用
let wipFiber = currentFiber.alternate;
// 初回レンダリングなどで alternate が存在しない場合
if (wipFiber === null) {
// 新たな Fiber ノードを生成
wipFiber = new FiberNode(currentFiber.tag, newProps, currentFiber.key, currentFiber.mode);
// 相互参照を設定(二重バッファリング)
wipFiber.alternate = currentFiber;
currentFiber.alternate = wipFiber;
}
// 属性のコピーなどの処理を省略...
return wipFiber;
}
この処理により、currentFiber(既存のツリー)と wipFiber(作業中のツリー)が alternate プロパティを通じて双方向にリンクされます。これにより、React は現在の UI 状態と作業中の UI 状態を同時に保持することが可能になります。
createWorkInProgress の戻り値である wipFiber は、FiberNode のインスタンスであり、実質的に currentFiber の複製です。この状態から、実際の処理ループである executeSyncLoop(元の workLoopSync)へと制御が移ります。
function executeSyncLoop() {
// 処理すべきノードが残っている限りループ
while (wipRoot !== null) {
processUnitOfWork(wipRoot);
}
}
このループ内では、processUnitOfWork が呼び出され、さらにその内部で beginWork が実行されます。新しい Fiber ノードが生成されると、それが次のループでの処理対象(wipRoot)として設定され、ツリー全体の構築が進められます。
beginWork によるノード処理の分岐
beginWork 関数は、Fiber ノードのタイプ(tag)に応じて適切な更新処理を委譲するスイッチ役です。ソースコードは膨大ですが、核心部分は以下の構造になっています。
function startWorkOnFiber(currentFiber, wipFiber, renderLanes) {
let hasUpdates = false;
// 既存ノードがある場合、props やコンテキストの変更をチェック
if (currentFiber !== null) {
const oldProps = currentFiber.memoizedProps;
const newProps = wipFiber.pendingProps;
if (oldProps !== newProps || hasContextChanged() || wipFiber.type !== currentFiber.type) {
hasUpdates = true;
} else {
// 更新が不要な場合の早期リターン処理など
if (!needsUpdateInOtherCases) {
hasUpdates = false;
}
}
}
// タグに基づいて処理を振り分け
switch (wipFiber.tag) {
case HostRoot:
return processRootNode(currentFiber, wipFiber, renderLanes);
case HostComponent:
return processDomNode(currentFiber, wipFiber, renderLanes);
case HostText:
return processTextNode(currentFiber, wipFiber);
// 他のケース...
default:
throw new Error("Unknown work tag");
}
}
ルートノード(HostRoot)や DOM ノード(HostComponent)など、タイプごとに processXXX 関数が呼び出されます。これらの関数に共通する重要な処理が、子ノードの調整を行う adjustChildFibers(元の reconcileChildren)の呼び出しです。
function adjustChildFibers(currentFiber, wipFiber, nextChildren, renderLanes) {
if (currentFiber === null) {
// 新規マウントの場合
wipFiber.child = mountChildFibers(wipFiber, null, nextChildren, renderLanes);
} else {
// 更新の場合
wipFiber.child = reconcileChildFibers(wipFiber, currentFiber.child, nextChildren, renderLanes);
}
}
この関数は、子ノードが新規作成されるべきか、既存のノードを再利用すべきかを判断し、実際の作業は ChildReconciler によって行われます。
ChildReconciler と副作用の管理
reconcileChildFibers と mountChildFibers は、どちらも createReconciler 関数によって生成されます。両者の違いは、副作用(Side Effects)の追跡を行うかどうかです。
const reconcileChildFibers = createReconciler(true);
const mountChildFibers = createReconciler(false);
createReconciler 内部では、ノードの追加、削除、更新を行うための関数群が定義されています。特に重要なのが、副作用フラグの設定です。
function markPlacement(newFiber) {
// 副作用の追跡が有効かつ、alternate が存在しない(新規)場合
if (shouldTrackSideEffects && newFiber.alternate === null) {
newFiber.effectFlags = Placement;
}
return newFiber;
}
ここで設定される effectFlags(旧バージョンでは effectTag)は、commit 段階でどのような DOM 操作を行うかを指示するビットフラグです。例えば Placement は、新しい DOM ノードを挿入する必要があることを示します。
初回レンダリング時には、ルートノードの子ノード作成処理が reconcileSingleElement などに委譲され、対応する Fiber ノードが生成されて Placement フラグが付与されます。これにより、生成された Fiber ツリーは、単なるデータ構造ではなく、後の commit 段階で必要な操作情報を保持した状態になります。
Fiber ツリーの構造と連結リスト化
executeSyncLoop による繰り返し処理を通じて、コンポーネント階層に対応するすべての Fiber ノードが生成されます。例えば、以下の JSX 構造があった場合、各要素に対応する Fiber ノードが順次作成されます。
function App() {
return (
<div className="App">
<div className="container">
<h1>タイトル</h1>
<p>テキスト 1</p>
<p>テキスト 2</p>
</div>
</div>
);
}
生成された Fiber ノード間は、以下の 3 つのプロパティによって連結されます。
child: 最初の子ノードへのポインタsibling: 次の兄弟ノードへのポインタreturn: 親ノードへのポインタ
例えば、h1 要素の Fiber ノードを確認すると、child は null(テキストノードは最適化により省略される場合がある)、return は親である div.container、sibling は次の兄弟である p タグを指しています。
このように、React の Fiber ツリーは、木構造のように見えて实则は各ノードがポインタで繋げた連結リストとして実装されています。これにより、再帰を使わずにイテレーションでツリーを走査し、処理の中断と再開(タイムスlicing)を可能にしています。
render 段階では、この workInProgress ツリーが構築され、各ノードに適切な副作用フラグが設定されます。このツリー構造とフラグ情報に基づき、次の commit 段階で実際の DOM 操作が実行されることになります。