Reactが初回マウントされるまでの仕組みを理解する
今回はReactが初回マウントされるまでの実装を私自身が学習した流れに沿って解説したいと思います。「React Internals Deep Dive」というブログ記事がReactの内部実装を知るのに大変参考になります。
また、「React Internals Explorer」を使うとReactが実行するプロセスを視覚的に理解することができるため、大変おすすめです。
はじめに
本記事では以下の構成に従って解説をしていきます。
- 前提として理解するべき要素
- FiberNodeの種類
- 4つの実行フェーズ
- currentとworkInProgress
- Trigger フェーズの実装
- Render フェーズの実装
- Commit フェーズの実装
初回マウントに関する内容は主にこちらのブログを参照しています。
なぜ初回マウントに限定するのか
今回はReactの実行の中でも初回マウントに限定して解説をします。Reactといえば再レンダリング時に最小限の差分を検知してDOMを操作する仕組み(Reconciliation)を備えています。しかし、再レンダリングの挙動を考慮すると実装の理解が複雑になってしまうことを懸念しました。
そこで、まずはReactの初回マウントに限定して解説することで、Reactの内部実装に関して土台となる重要な要素を把握することをこの記事の目的とします。
また同様にHydrationやConcurrent Modeなどの機能は理解を難しくすると考えるため今回の記事の中では扱いません。
加えて、私自身も現在Reactの内部構造について勉強中の身ですので、本記事の中に誤解を招く記述がありましたらご指摘いただけますと幸いです。
前提として理解するべき要素
まず、Reactの内部実装を理解する前提として把握するべきポイントを解説します。これらの知識をすでにご存知の方は本章はスキップしていただいて良いかと思います。
1. FiberNodeの種類
まずはFiberNodeの種類と要素について整理します。
React Fiberは、アプリケーションの状態を内部的に表現するためのアーキテクチャです。
参考: https://jser.dev/2023-07-14-initial-mount/#1-brief-introduction-on-fiber-architecture
FiberRootNode
かFiberNode
のいずれかで構成されたツリー状の構造になっています。ReactのランタイムはこのFiber Treeを効率的に扱い、DOMへの更新を最小限に抑えるよう努力しています。
FiberRootNode
FiberRootNodeはルートとして機能する特別なノードで、アプリ全体に関する必要なメタ情報を保持します。そのcurrentがFiber Treeの実体を指し、新しいFiber Treeが構築されるとcurrentが新しいHostRootを指し直します。
FiberNode
FiberRootNode以外は全てFiberNodeです。FiberNodeは大まかに以下の要素を持ちます。
tag
- tagには多数種類があり
HostRoot
やFunctionComponent
などそれぞれが識別されます。実際にこのtagを見て処理を分岐させるコードがいくつか見られます。 -
HostRoot
はFiberRootNode直下に位置し、Fiber Treeの起点になる特別なtagです。
stateNode
- tagがHostComponentの場合、実際のDOM Nodeを管理します。stateNodeによって実際のDOM Nodeと効率的に関連付けをしています。
child, sibling, return
- これらを組み合わせてFiber Treeを構築しています。
- React FiberがどのようにTreeを探索するかは下記のブログが参考になります。
flags
- Commit フェーズで更新を適用するかを判定するフラグ。
lanes, childLanes
- 更新の優先度を示すために使用されます。
- childLanesはその子孫ノード全てを含むサブツリーの更新の優先度を保持します。
- Laneは32ビットの整数値で表現されています
memoizedState
- tagがFunctionComponentの場合、memoizedStateはフックに関連するデータを保持します。
2. 大きく4つの実行フェーズに分けられる
次にReactがレンダリングしUIが描画されるまでに実行されるフェーズを整理します。
Reactの実行フェーズをどのように分けるかは記事によってばらつきが見られますが、ここでは以下の4つの実行フェーズが存在する前提で解説をします。
- Trigger フェーズ
- Schedule フェーズ
- Render フェーズ
- Commit フェーズ
参考:https://jser.dev/2023-07-11-overall-of-react-internals#3-the-overview-of-react-internals
こちらの画像に掲載されているフェーズと主要な関数について簡単に見ていきます。
Trigger フェーズ
初回マウントと再レンダリング時で共通してTrigger フェーズが全ての作業の始点であり、ReactDOMRoot.render()
もしくはsetState()
がTrigger フェーズの起因になります。
そして、Trigger フェーズの中でscheduleUpdateOnFiber()
が実行され、レンダリングする対象をReactのランタイムに伝えます。
Schedule フェーズ
React SchedulerはタスクをPriority Queueに格納し、優先順位に基づいて効率的に処理します。scheduleCallback()
はタスクをスケジュールするための関数で、実行されるタスクに優先順位を設定します。
workLoop()
は実際にタスクを実行するループです。このループは、Priority Queueからタスクを取り出し順に実行します。
今回は初回マウントにのみ焦点を当てるため、Schedule フェーズの役割である優先度の管理には着目しません。タスクは中断や優先度の変更を考慮せずに連続的に処理が行われる前提で解説を進めます。
Render フェーズ
Render フェーズではスケジュールされたタスクを実行し、新しいFiber Treeを構築してDOMに変更が必要な対象を整理します。
performConcurrentWorkOnRoot()
はTrigger フェーズで作成され、Schedule フェーズによって優先づけされてRender フェーズにて実行されます。
Commit フェーズ
Render フェーズによって新しいFiber Treeが構築され、変更をするべき箇所を把握することができました。Commit フェーズではこの更新対象をDOMに適用します。
このように、Reactの実行は大きく4つの段階に分けることができます。後の章にてこれらのフェーズをそれぞれ詳しく確認していきます。
3. currentとworkInProgress
先ほどFiberNodeの各要素について重要なものを抜粋して解説しました。最後にReact Fiberの構築において度々出てくるcurrent
とworkInProgress
が何を意味しているのか整理します。
上記のコードは各FiberNodeごとに実行される関数ですが、current
とworkInProgress
をそれぞれ引数に取っています。このように双方を引数に受け取る実装は度々出てきます。
current
はUIに描画される現在のバージョンのFiberNodeであり、workInProgressは新たに構築しているFiberNodeになります。
Reactはこれらを比較することによって差分を検知し、DOMの更新を最小限に抑える努力をしています。
そして注目するべき点はcurrent
がNullableであるという点です。特に初回マウント時は現在描画されているUIがないため、current
はNullになります。
Trigger フェーズの実装
これまででReactの内部実装を読み進めるために必要な知識を整理しました。ここからはTrigger フェーズ、Render フェーズ、Commit フェーズと実装を見ていきます。
先ほど説明した通りConcurrent Modeを本記事では扱わないため、タスクは中断や優先度の変更を考慮せずに連続的に処理が行われるものとし、Schedule フェーズの解説をスキップします。
createRoot
普段Reactを使う際に以下のようなコードを書いています。まずはcreateRootがどのような役割を持っているのかを整理しましょう。
const container = document.getElementById('app');
const root = createRoot(container);
root.render(<App />);
createRootの第一引数にはDOM要素を渡します。そしてこのDOM要素に対応するルートを作成し、renderとunmountの2つのメソッドを持つオブジェクトを返します。
createRootの内部でFiberRootNode
を作成します。FiberRootNode
は再レンダリングが発生したとしても新しく作り直されることはありません。
またcreateRootの中でFiberRootNode
に加えてダミーのHostRoot
を作成し、FiberRootNode
のcurrentをダミーのHostRoot
へ向けています。
これはHostRootとFiberRootNodeとの間に双方向リンクを確立する目的や、初回マウントと再レンダリングでロジックを共通化するための工夫かと推測します。
root.render()
先ほどcreateRootで作成したrootからrenderを実行して更新をスケジュールします。root.render()が実行されると、内部でupdateContainer
が呼ばれます。
updateContainer
を見ていきましょう。引数にはroot.render(<App />)
で指定したReactNodeのList(<App />
)と、createRootで作成されたrootのインスタンスが渡されます。
簡略化したupdateContainer
の実装は以下の流れになります。更新オブジェクトを作成し、キューに追加して更新をスケジュールします。
// 更新オブジェクトを作成する
const update = createUpdate(lane);
// 更新オブジェクトのpayloadにelementを設定する
update.payload = {element};
// 生成した更新オブジェクトをFiberノードの更新キューに追加
const root = enqueueUpdate(current, update, lane);
if (root !== null) {
// scheduleUpdateOnFiber関数を呼び出して、Fiberツリーの更新をスケジュール
scheduleUpdateOnFiber(root, current, lane);
entangleTransitions(root, current, lane);
}
Trigger フェーズの役割はここまでです。
Render フェーズ
先ほどのTrigger フェーズにて基本になるFiberNodeを作成し、更新するための処理をキューにスケジュールしました。次にReactがどのようにFiber Treeを構築していくのかを確認します。
performConcurrentWorkOnRoot
performConcurrentWorkOnRoot
は初回マウント、再レンダリングに関わらずRender フェーズのエントリーポイントになる関数です。
関数名にconcurrent
と入っていますが必ずしも並行に処理されるわけではなく、初回マウント時などは同期的に処理されることに注意です。初回マウント時にはレンダリングが中断されることはないため、最短でUIを描画することに集中しています。
function performConcurrentWorkOnRoot(root, didTimeout) {
// ...
let lanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
const shouldTimeSlice =
!includesBlockingLane(root, lanes) &&
!includesExpiredLane(root, lanes) &&
(disableSchedulerTimeoutInWorkLoop || !didTimeout);
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
// ↓ ⭐️ 初回マウントではこちらが実行される
: renderRootSync(root, lanes);
// ...
}
renderRootSync
performConcurrentWorkOnRoot
の中でrenderRootConcurrent
を実行するかrenderRootSync
を分岐していました。
初回マウントでは同期的にレンダリングがされるため、renderRootSync
の中を見ていきます。
重要な処理を抜粋して整理したものが下のコードになります。
function renderRootSync(root: FiberRoot, lanes: Lanes) {
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
// ...
// ⭐️ Fiber Treeの構築を開始するための準備
prepareFreshStack(root, lanes);
}
// ⭐️ Fiber Treeの構築が完了するまでLoopする
do {
try {
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
}
function workLoopSync() {
while (workInProgress !== null) {
// ⭐️ FiberNode単位で実行
performUnitOfWork(workInProgress);
}
}
renderRootSync
が実行する内容は大きく2つあります。
-
prepareFreshStack
によってFiber Treeの構築を開始するために準備をすること。 -
workInProgressがnullになる(次に処理するFiber Nodeが無くなる)まで
performUnitOfWork
を実行してFiber Treeを構築していくこと。
それぞれの関数の中身を見ていきます。
prepareFreshStack
prepareFreshStack
のコードを見るとFiber Treeを構築する前提として必要な設定を行なっていることがわかります。
function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
// ...
if (workInProgress !== null) {
let interruptedWork = workInProgress.return;
while (interruptedWork !== null) {
unwindInterruptedWork(interruptedWork, workInProgressRootRenderLanes);
interruptedWork = interruptedWork.return;
}
}
workInProgressRoot = root;
// ⭐️ workInProgressを作成する
workInProgress = createWorkInProgress(root.current, null);
workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
workInProgressRootExitStatus = RootIncomplete;
workInProgressRootFatalError = null;
workInProgressRootSkippedLanes = NoLanes;
workInProgressRootInterleavedUpdatedLanes = NoLanes;
workInProgressRootRenderPhaseUpdatedLanes = NoLanes;
workInProgressRootPingedLanes = NoLanes;
enqueueInterleavedUpdates();
// ...
}
この中でも特にcreateWorkInProgress
のコードに注目します。
すると、workInProgressがnullの場合は新しいFiberNodeを作成しますが、すでに存在する際には既存のworkInProgress(FiberNode)を再利用するようにしています。
Reactはこのようにインスタンスを再利用することで、必要以上のメモリ使用を回避する最適化を行なっています。
performUnitOfWork
renderRootSync
にて、prepareFreshStack
が完了したのちにFiber Treeの構築が完了するまでperformUnitOfWork
をWhileループで実行していました。
performUnitOfWork
はFiberNodeごとに実行されます。
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
workLoopSync
で実行されるperformUnitOfWork
の終了条件はworkInProgressがnullになることでした。
細かい処理を省略しますが、performUnitOfWork
ではworkInProgress(処理対象のFiberNode)に対してbeginWork
を実行し、next(次に処理をしたいFiberNode)があればworkInProgressに代入しています。
let next;
// ...
next = beginWork(current, unitOfWork, subtreeRenderLanes);
if (next === null) {
// ⭐️ 次に処理する対象がない場合は完了の処理をする
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
そしてnextが存在しない、つまりFiber Treeのchildが無くなるまで処理が完了した場合はcompleteUnitOfWork
を実行してworkLoopSync
から抜け出すのがperformUnitOfWork
の役割になります。
beginWork
ではperformUnitOfWork
でFiberNodeごとに実行されているbeginWork
はどのような処理を実行しているのでしょうか?
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
// ...
// ⭐️ FiberNodeのtagごとに処理を分岐
switch (workInProgress.tag) {
case IndeterminateComponent: {
return mountIndeterminateComponent(
current,
workInProgress,
workInProgress.type,
renderLanes,
);
}
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes);
case HostText:
return updateHostText(current, workInProgress);
}
}
beginWork
のコードは非常にわかりやすくworkInProgressのtagを見て実行する処理を振り分けています。
例えばtagがFiberRootNode直下のHostRootの場合はupdateHostRoot
が、HTML Tagなどの要素を表すHostComponentの場合はupdateHostComponent
が実行されるといった流れです。
Fiber Treeの構造上、FiberRootNodeの下に存在するのはHostRootのtagを持つFiberNodeでした。
つまり、初回のbeginWork
ではHostRootのtagを持つFiberNodeが処理されるため、まずはupdateHostRoot
が実行されます。
renderRootSync
の内部ではprepareFreshStack
でFiber Tree構築の準備をし、workInProgressがnullになるまでperformUnitOfWork
を実行して、完了次第completeUnitOfWork
を呼んでいました。
この流れを一度整理すると下の画像のようになるかと思います。
updateHostRoot
ではbeginWorkのtagで分岐されていたコードの内部では何が実行されているのでしょうか?
まずHostRootのFiberNodeが処理されるということで、updateHostRoot
の内容を見ていきましょう。下記のコードは少し省略して書いています。
function updateHostRoot(
current: null | Fiber,
workInProgress: Fiber,
renderLanes: Lanes,
) {
pushHostRootContext(workInProgress);
const nextProps = workInProgress.pendingProps;
const prevState = workInProgress.memoizedState;
const prevChildren = prevState.element;
// 更新キューをクローンして処理する
cloneUpdateQueue(current, workInProgress);
processUpdateQueue(workInProgress, nextProps, null, renderLanes);
const nextState: RootState = workInProgress.memoizedState;
const nextChildren = nextState.element;
if (nextChildren === prevChildren) {
// 変更がない場合は、早期に処理を終了
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
// 変更がある場合は差分を調整する
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
// 更新されたFiberNodeのchildを返却
return workInProgress.child;
}
processUpdateQueueが実行されると対象のFiberNodeの状態を更新し、新しい状態を計算します。
その後、変更前後のelementを比較することによって差分が存在するかを検知しています。変更がない場合は、早期に処理を終了することが可能なため、bailoutOnAlreadyFinishedWork
を実行して処理を完了します。
反対に差分が生じた場合にはreconcileChildren
を実行して差分を調整します。
reconcileChildren
reconcileChildren
は差分を調整する非常に重要な役割を担っています。この関数はupdateHostRoot
以外のupdate...
関数でも使用されています。
内部ではcurrentがnullか否かによってmountChildFibers
かreconcileChildFibers
のどちらを実行するか分岐させています。
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes,
) {
if (current === null) {
// ⭐️ 初回マウントでは基本↓になる
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
} else {
// ⭐️ HostRootのみ↓になる
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes,
);
}
}
先ほどから度々登場するcurrentのnullチェックですが、初回マウント時はHostRoot以外currentが存在しないためnullになります。
”HostRoot以外”という点が重要で、冒頭で解説したようにcreateRootによってFiberRootNodeに加えてダミーのHostRootを作成していました。
画像の通り、HostRootのみcurrentが存在するためmountChildFibers
ではなくreconcileChildFibers
が実行されます。
HostRootでreconcileChildFibers
が実行されることで、レンダリングプロセスの起点が固定され初回マウントと再レンダリングのロジックが共通化しやすくなるメリットがあります。
reconcileChildFibers
とmountChildFibers
の違いはcreateChildReconciler
の引数に渡すBooleanのみです。これによってshouldTrackSideEffects
の値がtrueかfalseか決まります。
export const reconcileChildFibers: ChildReconciler = createChildReconciler(true);
export const mountChildFibers: ChildReconciler = createChildReconciler(false);
function createChildReconciler(
shouldTrackSideEffects: boolean,
): ChildReconciler {
// ...
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
// ⭐️ reconcileの実行
const firstChildFiber = reconcileChildFibersImpl(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
return firstChildFiber;
}
return reconcileChildFibers;
}
createChildReconciler
の内部ではさらにreconcileChildFibersImpl
という関数が定義されています。この関数が実際にreconcileするための処理を担っています。
function reconcileChildFibersImpl(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
// ...
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
case REACT_PORTAL_TYPE:
return placeSingleChild(
reconcileSinglePortal(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
case REACT_LAZY_TYPE:
const payload = newChild._payload;
const init = newChild._init;
return reconcileChildFibers(
returnFiber,
currentFirstChild,
init(payload),
lanes,
);
}
// ...
}
// ...
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
更に内部では$$typeof
というReact Elementのtypeによって処理をswitchさせています。
それぞれ具体的な処理は異なりますが、おおよそreconcile...
(例: reconcileSingleElement)で必要な調整処理を実行し、placeSingleChild
でDOMの挿入が必要であることをマークする実装になっています。
reconcileSingleElement
ここでは$$typeof
がREACT_ELEMENT_TYPEだったと仮定してreconcileSingleElement
を見ていきます。
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes,
): Fiber {
// ...
let child = currentFirstChild;
while (child !== null) {
// ...
// すでにchildが存在する場合は別途処理が入りますが、初回マウント時にはchildが存在しません。
}
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
// ...
}
すると、createFiberFromElement
関数を使用してReact Elementから新しいFiberNodeを作成していることがわかります。
そしてcoerceRef
で適当な形へ変換し、created
のreturnに親Fiberを登録します。
placeSingleChild
次にplaceSingleChild
です。
placeSingleChildは非常に単純で、新しく作成されたFiberNodeに対して、DOMへの挿入が必要かどうかを判断し、必要な場合はPlacement
flagを設定します。
ここでまた、createRootでダミーのHostRootを作成していた実装が関係してきます。
初回マウントではHostRoot以外はcurrentが存在しないためshouldTrackSideEffectsがfalseでした。しかし、ダミーのHostRootがあることによって、updateHostRoot
で実行されるreconcileChildren
のみreconcileChildFibers
が実行され、shouldTrackSideEffects
がtrueになります。
// 再掲
export const reconcileChildFibers: ChildReconciler = createChildReconciler(true);
export const mountChildFibers: ChildReconciler = createChildReconciler(false);
function createChildReconciler(
shouldTrackSideEffects: boolean,
): ChildReconciler {
// ...
}
これにより、初回マウント時も再レンダリング時と共通の処理でHostRootのchildに対してPlacementのflagを設定することができる仕組みになっています。
このようにして、先ほどのreconcileChildFibersImplで行われていた処理が実行されます。
そしてこの処理を繰り返すことにより、最終的には以下のようなFiber Treeが構築されます。
completeWork
これまででperformUnitOfWorkを繰り返し、React ElementからFiber Treeを構築してきました。
Render フェーズでは最後に実際に挿入するためのDOM Nodeを作成してstateNode
に追加する処理を行います。
再度performUnitOfWork
のコードを掲載します。
let next;
// ...
next = beginWork(current, unitOfWork, subtreeRenderLanes);
if (next === null) {
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
すると、nextがnullになった際にcompleteUnitOfWork
を実行していることがわかります。
completeUnitOfWork
のコードはこちらです。注目するべきは内部で呼ばれているcompleteWork
です。
completeWork
completeWorkの内部を見ていきましょう。
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
popTreeContext(workInProgress);
switch (workInProgress.tag) {
...
case HostComponent: {
popHostContext(workInProgress);
const type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
// ...
} else {
...
if (wasHydrated) {
...
} else {
const rootContainerInstance = getRootHostContainer();
// ⭐️ 実際のDOM Nodeを作成
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
// ⭐️ サブツリーのDOM Nodeを含めてappend。この時点ではまだInsertしていない
appendAllChildren(instance, workInProgress, false, false);
// ⭐️ stateNodeに代入
workInProgress.stateNode = instance;
// ...
}
}
...
return null;
}
...
}
}
解説が必要な箇所のみに省略をしていますが、createInstance
によって実際のDOMのインスタンスが作成されていることがわかります。
そして、appendAllChildren
でサブツリーのDOM Nodeを全てappendしたDOM Nodeを作成し、workInProgresのstateNode
に代入しています。
この処理をbeginWorkが完了したのち、Fiber Treeを逆に戻るように実行することで必要なDOM Nodeのインスタンスを作成します。
Commit フェーズ
これまでの処理によって、workInProgressのFiber Treeが完成し、必要なDOM Nodeが作成され、DOMの操作が必要なFiberNodeにフラグをつけることができました。
ここまで来れば、あとはDOMを実際に挿入する処理が行えます。
commitMutationEffects
performConcurrentWorkOnRoot
関数をもう一度確認します。すると、renderRootSyncによってRender フェーズが実行され、Fiber Treeが構築されたのちにfinishConcurrentRender
という関数が呼ばれています。
function performConcurrentWorkOnRoot(root, didTimeout) {
// ...
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
// ↓ 初回マウントではこちらが実行される
: renderRootSync(root, lanes);
// ...
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
// ⭐️ ↓の関数がrenderRoot...の後に実行
finishConcurrentRender(root, exitStatus, finishedWork, lanes);
// ...
}
そして、finishConcurrentRenderの中でcommitMutationEffects
が実行されており、この関数が実際にDOMの操作をしています。
commitMutationEffectsではcommitMutationEffectsOnFiber
が実行されています。
export function commitMutationEffects(
root: FiberRoot,
finishedWork: Fiber,
committedLanes: Lanes,
) {
inProgressLanes = committedLanes;
inProgressRoot = root;
commitMutationEffectsOnFiber(finishedWork, root, committedLanes);
inProgressLanes = null;
inProgressRoot = null;
}
commitMutationEffectsOnFiber
では処理を再帰的に実行しており、FiberNodeのtagを見て処理をswitchしています。
また、下記のコードを見て分かる通り共通してrecursivelyTraverseMutationEffects
とcommitReconciliationEffects
が実行されています。
function commitMutationEffectsOnFiber(
finishedWork: Fiber,
root: FiberRoot,
lanes: Lanes,
) {
const current = finishedWork.alternate;
const flags = finishedWork.flags;
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
...
return;
}
...
case HostComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
...
return;
}
case HostText: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
...
return;
}
case HostRoot: {
if (enableFloat && supportsResources) {
prepareToCommitHoistables();
const previousHoistableRoot = currentHoistableRoot;
currentHoistableRoot = getHoistableRoot(root.containerInfo);
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
currentHoistableRoot = previousHoistableRoot;
commitReconciliationEffects(finishedWork);
} else {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
}
...
return;
}
...
default: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
return;
}
}
}
recursivelyTraverseMutationEffects
ではrecursivelyTraverseMutationEffects
を見ていきます。
function recursivelyTraverseMutationEffects(
root: FiberRoot,
parentFiber: Fiber,
lanes: Lanes,
) {
const deletions = parentFiber.deletions;
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const childToDelete = deletions[i];
try {
// ⭐️ 要素をremoveChildする
commitDeletionEffects(root, parentFiber, childToDelete);
} catch (error) {
captureCommitPhaseError(childToDelete, parentFiber, error);
}
}
}
const prevDebugFiber = getCurrentDebugFiberInDEV();
if (parentFiber.subtreeFlags & MutationMask) {
// 再帰的に実行
let child = parentFiber.child;
while (child !== null) {
setCurrentDebugFiberInDEV(child);
commitMutationEffectsOnFiber(child, root, lanes);
child = child.sibling;
}
}
setCurrentDebugFiberInDEV(prevDebugFiber);
}
recursivelyTraverseMutationEffects
という名前の通り、この関数も再帰的に実行されます。
そして、parentFiberのdeletions
から削除される予定の子要素を取得して、commitDeletionEffectsによって要素をremoveChildします。
commitReconciliationEffects
recursivelyTraverseMutationEffects
がDOMの変異に対応したのに対し、commitReconciliationEffects
は実際にDOMの挿入などを行います。
ここではRender フェーズの際に付与したflagを確認して、Placement
がmarkされていた場合はcommitPlacement
を実行します。
commitPlacement
ではparentFiberからstateNode
を取得して、insertOrAppendPlacementNode
を実行してDOMに挿入しています。
このようにCommit フェーズではRender フェーズまでで用意されたFiber Treeを元に実際にDOMの操作を実施する役割を持っています。
そしてDOMが実際に描画されFiberRootNodeのcurrentが、今までworkInProgressとして扱われていたFiber Treeを指すことによって、次のレンダリングではcurrentが現在画面に描画されているFiber Treeになります。
まとめ
今回はReactの初回マウントが実行されてUIが描画されるまでを見てきました。本来はこれに加えて再レンダリングやConcurrent Modeを考慮した、React SchedulerやReact Laneによる優先付けなども絡んできます。
ですが、基本になる流れは初回マウントで最もシンプルにレンダリングされるケースであると考えています。今回の内容をベースに様々なケースでどのようにレンダリングが実行されていくかを今後とも見ていきたいなと思います!
最後に
AI Shiftではエンジニアの採用に力を入れています!
少しでも興味を持っていただけましたら、カジュアル面談でお話しませんか?
(オンライン・19時以降の面談も可能です!)
【面談フォームはこちら】
Discussion