React の初期マウントは内部でどのように行われるのでしょうか?
React Internals Deep Dive のエピソード 2 です。解説動画はYouTube で視聴可能
React@18.2.0 に基づいています。新しいバージョンでは実装が変更されている可能性があります
React の内部構造概要で簡単に触れたように、React は内部で Fiber Tree と呼ばれるツリー構造を使用して最小限の DOM 更新を計算し、コミットフェーズで確定します。この記事では、React が初期マウント(初回レンダリング)をどのように行うのかを詳細に解説します。具体的には、以下のコードから DOM がどのように構築されるのかを追っていきます。
1. Fiber アーキテクチャの概要
Fiber は React の内部状態の表現方法です。これは FiberRootNode と FiberNodes で構成されるツリー構造です。FiberNode にはさまざまな種類があり、一部にはバッキング DOM ノードを持つものがあります(HostComponent)。
React ランタイムは Fiber Tree を維持し、更新し、最小限の更新でホスト DOM を同期します。
1.1 FiberRootNode
FiberRootNode は React のルートです。これはアプリ全体に関する必要なメタ情報を保持します。そのcurrent
は実際の Fiber Tree を指し、新しい Fiber Tree が構築されるたびにそのcurrent
を新しいHostRoot
に再ポイントします。
1.2 FiberNode
FiberNode は FiberRootNode 以外のすべてのノードを意味します。重要なプロパティのいくつかは次のとおりです。
tag
: FiberNode には多くのサブタイプがあり、tag
によって区別されます。たとえば、FunctionComponent、HostRoot、ContextConsumer、MemoComponent、SuspenseComponent など。stateNode
: 他のバッキングデータを指すために使用されます。HostComponent
の場合、stateNode
は実際のバッキング DOM ノードを指します。child
、sibling
、return
: これらは一緒にツリー構造を形成します。elementType
は、提供されたコンポーネント関数または組み込み HTML タグです。flags
: Commit フェーズで適用する更新を示すために使用されます。subtreeFlags
はそのサブツリー用です。lanes
: 保留中の更新の優先度を示すために使用されます。childLanes
はそのサブツリー用です。memoizedState
: 重要なデータを指すために使用されます。FunctionComponent の場合、これはフックを意味します。
2. Trigger phase での初期マウント
createRoot()
は React ルートを作成し、current
としてダミーの HostRoot FiberNode を作成します。
export function createRoot(
container: Element | Document | DocumentFragment,
options?: CreateRootOptions,
): RootType {
let isStrictMode = false;
let concurrentUpdatesByDefaultOverride = false;
let identifierPrefix = '';
let onRecoverableError = defaultOnRecoverableError;
let transitionCallbacks = null;
// ~ ここでFiberRootNodeが返されます
const root = createContainer(
container,
ConcurrentRoot,
null,
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onRecoverableError,
transitionCallbacks,
);
markContainerAsRoot(root.current, container);
Dispatcher.current = ReactDOMClientDispatcher;
const rootContainerElement: Document | Element | DocumentFragment =
container.nodeType === COMMENT_NODE
? (container.parentNode: any)
: container;
listenToAllSupportedEvents(rootContainerElement);
return new ReactDOMRoot(root);
}
export function createContainer(
containerInfo: Container,
tag: RootTag,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
isStrictMode: boolean,
concurrentUpdatesByDefaultOverride: null | boolean,
identifierPrefix: string,
onRecoverableError: (error: mixed) => void,
transitionCallbacks: null | TransitionTracingCallbacks,
): OpaqueRoot {
const hydrate = false;
const initialChildren = null;
return createFiberRoot(
containerInfo,
tag,
hydrate,
initialChildren,
hydrationCallbacks,
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onRecoverableError,
transitionCallbacks,
);
}
export function createFiberRoot(
containerInfo: Container,
tag: RootTag,
hydrate: boolean,
initialChildren: ReactNodeList,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
isStrictMode: boolean,
concurrentUpdatesByDefaultOverride: null | boolean,
identifierPrefix: string,
onRecoverableError: null | ((error: mixed) => void),
transitionCallbacks: null | TransitionTracingCallbacks,
): FiberRoot {
// $FlowFixMe[invalid-constructor] Flowは関数のnew呼び出しをサポートしなくなりました
const root: FiberRoot = (new FiberRootNode(
containerInfo,
tag,
hydrate,
identifierPrefix,
onRecoverableError,
): any);
// 循環構造。stateNodeがanyのため、現在の型システムを回避しています
const uninitializedFiber = createHostRootFiber(
tag,
isStrictMode,
concurrentUpdatesByDefaultOverride,
);
// ~ HostRootのFiberNodeが作成され、React rootのcurrentとして割り当てられます
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
...
initializeUpdateQueue(uninitializedFiber);
return root;
}
root.render()
は HostRoot に更新をスケジュールします。要素の引数は更新ペイロードに保存されます。
function ReactDOMRoot(internalRoot: FiberRoot) {
this._internalRoot = internalRoot;
}
ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render =
function (children: ReactNodeList): void {
const root = this._internalRoot;
if (root === null) {
throw new Error("Cannot update an unmounted root.");
}
updateContainer(children, root, null, null);
};
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function
): Lane {
const current = container.current;
const lane = requestUpdateLane(current);
if (enableSchedulingProfiler) {
markRenderScheduled(lane);
}
const context = getContextForSubtree(parentComponent);
if (container.context === null) {
container.context = context;
} else {
container.pendingContext = context;
}
const update = createUpdate(lane);
// 注意: React DevTools は現在、"element" と呼ばれるこのプロパティに依存しています
// ~ render() の引数は update の payload に保存されます
update.payload = { element };
// ~ update は enqueue されます。この処理の詳細には立ち入りませんが
// ~ update が処理待ち状態になることを覚えておいてください
const root = enqueueUpdate(current, update, lane);
if (root !== null) {
scheduleUpdateOnFiber(root, current, lane);
entangleTransitions(root, current, lane);
}
return lane;
}
3. Render phase での初期マウント
3.1 performConcurrentWorkOnRoot()
React の内部構造概要で触れたように、performConcurrentWorkOnRoot()
は初期レンダリングと再レンダリングの両方のためにレンダリングを開始するエントリーポイントです。
1 つ注意すべき点は、それがconcurrent
と呼ばれているにもかかわらず、必要に応じて内部でsync
モードにフォールバックすることです。初期マウントはそのようなケースの 1 つです。なぜなら、DefaultLane はブロッキングランをブロッキングするためです。
function performConcurrentWorkOnRoot(root, didTimeout) {
...
// ルートに保存されているフィールドを使用して、次に作業するレーンを決定します。
let lanes = getNextLanes(
root,
root === workInProgres? workInProgressRootRenderLanes : NoLanes,
);
...
// 以下のケースではtime-slicingを無効化します:
// 長時間CPUバウンドな処理("expired" workによる starvation 防止)
// またはsync-updates-by-defaultモードの場合
// TODO: Schedulerのバグ対応として防御的に`didTimeout`をチェックしています
// Schedulerのバグが修正され次第、このチェックは不要になります(自前でexpirationを追跡しているため)
const shouldTimeSlice =
!includesBlockingLane(root, lanes) &&
!includesExpiredLane(root, lanes) &&
(disableSchedulerTimeoutInWorkLoop || !didTimeout);
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes);
...
}
// ~ Blockingは重要で、中断されるべきではない処理のことです
export function includesBlockingLane(root: FiberRoot, lanes: Lanes) {
const SyncDefaultLanes =
InputContinuousHydrationLane |
InputContinuousLane |
DefaultHydrationLane |
// ~ DefaultLaneはblocking lane
DefaultLane;
return (lanes & SyncDefaultLanes) !== NoLanes;
}
What are Lanes in Reactを参照してください。
上記のコードから、初期マウントの場合、concurrent モードは実際には使用されないことがわかります。これは理にかなっています。初期マウントの場合、UI をできるだけ早く描画することが助けになるため、それを延期するのは助けになりません。
3.2 renderRootSync()
renderRootSync()
は内部で単なる while ループです。
function renderRootSync(root: FiberRoot, lanes: Lanes) {
const prevExecutionContext = executionContext;
executionContext |= RenderContext;
const prevDispatcher = pushDispatcher();
// rootまたはlanesが変更された場合、既存のスタックを破棄して新規作成
// 変更ない場合は前回の続きから処理を再開
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
const memoizedUpdaters = root.memoizedUpdaters;
if (memoizedUpdaters.size > 0) {
restorePendingUpdaters(root, workInProgressRootRenderLanes);
memoizedUpdaters.clear();
}
// 今後のworkをスケジュールしたFibersをMapからSetに移動
// bailout発生時は元に戻します(上記同様)
// 異なるupdaterで同一優先度の追加workが生成される場合に備え、現在のupdateと
// 将来のupdateを分離するため、この時点で移動することが重要
movePendingFibersToMemoized(root, lanes);
}
}
workInProgressTransitions = getTransitionsForLanes(root, lanes);
prepareFreshStack(root, lanes);
}
do {
try {
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
resetContextDependencies();
executionContext = prevExecutionContext;
popDispatcher(prevDispatcher);
// 進行中のレンダーがないことを示すためnullを設定
workInProgressRoot = null;
workInProgressRootRenderLanes = NoLanes;
return workInProgressRootExitStatus;
}
// work loopは極めてホットなパスのためClosureのインライン化を抑制
/** @noinline */
function workLoopSync() {
// タイムアウト済みのためyieldチェックせずにworkを実行
// ~ このwhileループはworkInProgressが存在する限り
// ~ `performUnitOfWork()`を継続的に実行することを意味します
while (workInProgress !== null) {
// ~ 名前の通り、1つのFiber Node単位で処理を実行
performUnitOfWork(workInProgress);
}
}
ここでworkInProgress
の意味を説明する必要があります。
React のコードベースではcurrent
とworkInProgress
の接頭辞が至る所で見られます。React が内部状態を表現するために Fiber Tree を使用しているため、更新がある度に新しいツリーを構築し古いツリーとの diff を取る必要があります。つまり、current
は UI に描画されている現在のバージョンを指し、workInProgress
は構築中の次期current
として使用されるバージョンを意味します。
3.3 performUnitOfWork()
これは React が単一の Fiber Node を見て、何かが行われるかどうかを確認する場所です。
このセクションをより簡単に理解するために、最初のエピソードを確認することをお勧めします - How does React traverse Fiber tree internally.
function performUnitOfWork(unitOfWork: Fiber): void {
// このFiberの現在の状態はalternateにあります。理想的には
// 何もこれに依存すべきではありませんが、ここで依存することで
// workInProgressに追加のフィールドが必要なくなります
const current = unitOfWork.alternate;
let next;
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
next = beginWork(current, unitOfWork, subtreeRenderLanes);
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
next = beginWork(current, unitOfWork, subtreeRenderLanes);
}
resetCurrentDebugFiberInDEV();
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// 新しいworkが生成されない場合、現在のworkを完了します
completeUnitOfWork(unitOfWork);
} else {
// ~ 前述の通り、`workLoopSync()`は単なるwhileループで
// ~ workInProgressに対して`completeUnitOfWork()`を実行し続けます
// ~ ここでworkInProgressを割り当てることは、次に処理するFiber Nodeを設定することを意味します
workInProgress = next;
}
ReactCurrentOwner.current = null;
}
beginWork()
は実際のレンダリングが行われる場所です。
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
// ~ currentがnullでない場合、これは初期マウントではないことを意味します
if (current !== null) {
...
} else {
// ~ そうでない場合、初期マウントであり、当然更新は存在しません
didReceiveUpdate = false
...
}
// ~ 異なるタイプの要素を個別に処理します
switch (workInProgress.tag) {
// ~ IndeterminateComponentはまだインスタンス化されていない
// ~ ClassコンポーネントまたはFunctionコンポーネントを意味します
// ~ 一度レンダリングされると適切なタグが決定されます
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,
);
}
// ~ これはFiberRootNode配下のHostRootです
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
// ~ p、divなどの組み込みHTMLタグ
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes);
// ~ HTMLテキストノード
case HostText:
return updateHostText(current, workInProgress);
// ~ その他のタイプも存在します
case SuspenseComponent:
...
}
}
次は、レンダリング手順を実行します。
3.4 prepareFreshStack()
renderRootSync()
には重要なprepareFreshStack()
の呼び出しがあります。
function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
root.finishedWork = null;
root.finishedLanes = NoLanes;
...
workInProgressRoot = root;
// ~ rootのcurrentはHostRootのFiberNodeを指します
const rootWorkInProgress = createWorkInProgress(root.current, null);
workInProgress = rootWorkInProgress;
workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
workInProgressRootExitStatus = RootInProgress;
workInProgressRootFatalError = null;
workInProgressRootSkippedLanes = NoLanes;
workInProgressRootInterleavedUpdatedLanes = NoLanes;
workInProgressRootRenderPhaseUpdatedLanes = NoLanes;
workInProgressRootPingedLanes = NoLanes;
workInProgressRootConcurrentErrors = null;
workInProgressRootRecoverableErrors = null;
finishQueueingConcurrentUpdates();
return rootWorkInProgress;
}
つまり、新しいレンダリングが開始されるたびに、現在の HostRoot から新しいworkInProgress
が作成されます。これは新しい Fiber Tree のルートとして機能します。
したがって、beginWork()
内のブランチでは、最初にHostRoot
に移動し、次のステップはupdateHostRoot()
になります。
3.5 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);
// ~ この呼び出しは記事の冒頭で言及されたupdateを処理します
// ~ スケジュールされたupdateが処理され、ペイロードが抽出され
// ~ elementがmemoizedStateとして割り当てられることを覚えておいてください
processUpdateQueue(workInProgress, nextProps, null, renderLanes);
const nextState: RootState = workInProgress.memoizedState;
const root: FiberRoot = workInProgress.stateNode;
pushRootTransition(workInProgress, root, renderLanes);
if (enableTransitionTracing) {
pushRootMarkerInstance(workInProgress);
}
// 注意: React DevToolsは現在、このプロパティが"element"という名前であることに依存しています
// ~ ReactDOMRoot.render()の引数を取得できます!
const nextChildren = nextState.element;
if (supportsHydration && prevState.isDehydrated) {
...
} else {
// Rootはデハイドレートされていません。クライアント専用ルートか、既にハイドレート済みです
resetHydrationState();
if (nextChildren === prevChildren) {
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
// ~ ここではcurrentとworkInProgressの両方にchildが存在しません。
// ~ nextChildrenは<App/>です
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
}
return workInProgress.child;
// ~ reconciling後、workInProgressに新しいchildが作成されます
// ~ ここで返すことは、次にworkLoopSync()が処理することを意味します
}
3.6 reconcileChildren()
これは React の内部で非常に重要な関数です。その名前から、reconcile
をdiff
として扱うことができます。これは新しい子供たちを古い子供たちと比較し、workInProgress
の正しいchild
を設定します。
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes
) {
// ~ currentがnullの場合、これは初期マウントであることを意味します
if (current === null) {
// まだレンダリングされていない新規コンポーネントの場合、
// 副作用を最小限に抑えて子要素を更新しません。代わりに、
// レンダリング前にすべての子要素を追加します。これにより、
// 副作用の追跡を行わずにreconciliation処理を最適化できます。
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes
);
// ~ currentが存在する場合、これは再レンダーを意味し、reconcileを行います
} else {
// 現在の子要素がwork in progressと同じ場合、これらの子要素に対して
// まだ何の処理も開始していないことを意味します。したがって、
// cloneアルゴリズムを使用して現在の子要素のコピーを作成します。
// 既に進行中のworkがある場合、この時点では無効となるため破棄します。
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes
);
}
}
前述の通り、FiberRootNode は常にcurrent
を持つため、2 番目のブランチであるreconcileChildFibers
に進みます。ただしこれは初期マウントであるため、その子要素current.child
は null です。
また、workInProgress
が構築中でまだchild
を持たないため、workInProgress
にchild
を設定している点に注意してください。
3.7 reconcileChildFibers()
vs mountChildFibers()
reconciliation の目的は既存の要素を再利用することです。mount は reconciliation の特殊なプリミティブバージョンと捉えることができ、常にすべてを新規作成します。
実際のコードではこれら 2 つの関数は大きな違いはなく、同じクロージャを異なるフラグshouldTrackSideEffects
で使用しています。
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 {
// この間接参照は、最後に`thenableState`をリセットできるように存在しています。
// Closure によってインライン化されるはずです。
thenableIndexCounter = 0;
const firstChildFiber = reconcileChildFibersImpl(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
thenableState = null;
// 毎回最初に設定されるので、`thenableIndexCounter`を 0 にリセットする必要はありません。
// ~ reconciling後の最初の子Fiberが返され、workInprogressの子として設定されます。
return firstChildFiber;
}
return reconcileChildFibers;
}
完全な Fiber ツリーを構築する場合、すべてのノードが reconciling 後に「挿入が必要」とマークされるべきだと想像するかもしれませんが、実際にはルートノードのみの挿入で十分です!このmountChildFibers
は、この挙動を明示的に制御するための内部最適化と言えます。
function reconcileChildFibersImpl(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
// この関数は再帰的ではありません
// トップレベルの要素が配列の場合、fragmentではなく、子要素の集合として扱います
// ネストされた配列はfragmentノードとして扱われます
// 再帰処理は通常のフローで行われます
// キーなしトップレベルfragmentを配列として扱います
// <>{[...]}</> と <>...</> の曖昧性が生じますが
// 同じ方法で処理します
// TODO: Usableノードのように再帰を使用するべきか?
const isUnkeyedTopLevelFragment =
typeof newChild === 'object' &&
newChild !== null &&
newChild.type === REACT_FRAGMENT_TYPE &&
newChild.key === null;
if (isUnkeyedTopLevelFragment) {
newChild = newChild.props.children;
}
// オブジェクトタイプの処理
if (typeof newChild === 'object' && newChild !== null) {
// ~ この$$typeofはReact Elementのタイプを表します
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
// ~ 次の2つの関数に注目します
return placeSingleChild(
// ~ childrenがReact Element(例: <App/>)の場合
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;
// TODO: この関数は非再帰的であるべき
return reconcileChildFibers(
returnFiber,
currentFirstChild,
init(payload),
lanes,
);
}
// ~ childrenが配列の場合
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
if (getIteratorFn(newChild)) {
return reconcileChildrenIterator(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
if (typeof newChild.then === 'function') {
const thenable: Thenable<any> = (newChild: any);
return reconcileChildFibersImpl(
returnFiber,
currentFirstChild,
unwrapThenable(thenable),
lanes,
);
}
if (
newChild.$$typeof === REACT_CONTEXT_TYPE ||
newChild.$$typeof === REACT_SERVER_CONTEXT_TYPE
) {
...
}
throwOnInvalidObjectType(returnFiber, newChild);
}
// ~ 最もプリミティブなケース(テキストノードの更新)を処理
if (
(typeof newChild === 'string' && newChild !== '') ||
typeof newChild === 'number'
) {
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
currentFirstChild,
'' + newChild,
lanes,
),
);
}
// 残りのケースはすべて空として扱われます
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
ここで 2 つのステップが見て取れます。reconcileXXX()
で diff 処理を行い、
placeSingleChild()
で DOM への挿入が必要な fiber にマークを付けます。
3.8 reconcileSingleElement()
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes
): Fiber {
const key = element.key;
let child = currentFirstChild;
// ~ 既にchildが存在する場合のupdateを処理
// ~ 初期マウント時には存在しないため、ここでは無視します
while (child !== null) {
// TODO: key === nullかつchild.key === nullの場合、リストの最初のアイテムにのみ適用
if (child.key === key) {
const elementType = element.type;
if (elementType === REACT_FRAGMENT_TYPE) {
if (child.tag === Fragment) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props.children);
existing.return = returnFiber;
return existing;
}
} else {
if (
child.elementType === elementType ||
// このチェックはfalseパスでのみ実行されるようインライン化
(__DEV__
? isCompatibleFamilyForHotReloading(child, element)
: false) ||
// Lazyタイプは解決済みタイプとreconcileする必要あり
// 上記のHot Reloadingチェック後に実行する必要があります
// ホットリロードは再サスペンドしないためプロダクションと異なるセマンティクスを持つため
(typeof elementType === "object" &&
elementType !== null &&
elementType.$$typeof === REACT_LAZY_TYPE &&
resolveLazy(elementType) === child.type)
) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
existing.ref = coerceRef(returnFiber, child, element);
existing.return = returnFiber;
if (__DEV__) {
existing._debugSource = element._source;
existing._debugOwner = element._owner;
}
return existing;
}
}
// マッチしなかった場合
deleteRemainingChildren(returnFiber, child);
break;
} else {
deleteChild(returnFiber, child);
}
child = child.sibling;
}
if (element.type === REACT_FRAGMENT_TYPE) {
const created = createFiberFromFragment(
element.props.children,
returnFiber.mode,
lanes,
element.key
);
created.return = returnFiber;
return created;
} else {
// ~ 前のバージョンがないため、要素から新しいfiberを作成します
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
}
}
reconcileSingleElement()
の初期マウント時の処理は非常にシンプルです。
新しく作成された Fiber Node がworkInProgress
のchild
として設定される点に注目してください。
注意すべき点は、カスタムコンポーネントから Fiber Node が作成される場合、
そのタグはFunctionComponent
ではなくIndeterminateComponent
となることです。
export function createFiberFromElement(
element: ReactElement,
mode: TypeOfMode,
lanes: Lanes,
): Fiber {
let owner = null;
const type = element.type;
const key = element.key;
const pendingProps = element.props;
const fiber = createFiberFromTypeAndProps(
type,
key,
pendingProps,
owner,
mode,
lanes,
);
return fiber;
}
export function createFiberFromTypeAndProps(
type: any, // React$ElementType
key: null | string,
pendingProps: any,
owner: null | Fiber,
mode: TypeOfMode,
lanes: Lanes,
): Fiber {
let fiberTag = IndeterminateComponent;
// resolvedTypeは最終的なタイプが判明している場合に設定されます(lazyではない場合)
let resolvedType = type;
...
const fiber = createFiber(fiberTag, pendingProps, key, mode);
fiber.elementType = type;
fiber.type = resolvedType;
fiber.lanes = lanes;
return fiber;
}
3.9 placeSingleChild()
reconcileSingleElement()
は Fiber Node の reconciliation のみを行い、placeSingleChild()
は
DOM への挿入が必要な子 Fiber Node にマークを付ける場所です。
function placeSingleChild(newFiber: Fiber): Fiber {
// シングルチャイルドケースではよりシンプルです
// 新しい子要素の挿入にplacementのみ必要
// ~ このフラグはここで使用されます(他の場所でも同様)
if (shouldTrackSideEffects && newFiber.alternate === null) {
// ~ PlacementはDOMサブツリーの挿入が必要なことを意味
newFiber.flags |= Placement | PlacementDEV;
}
return newFiber;
}
この処理はchild
に対して行われることに注意してください。初期マウント時には、HostRoot
の子(デモコードでは<App/>
)が
Placement
フラグでマークされます。
3.10 mountIndeterminateComponent()
beginWork()
内の次のブランチとしてIndeterminateComponent
を見ていきます。<App/>
は HostRoot 配下にあり、前述の通りカスタムコンポーネントは最初IndeterminateComponent
としてマークされるため、初めて<App/>
が reconciled される際にここに到達します。
function mountIndeterminateComponent(
_current: null | Fiber,
workInProgress: Fiber,
Component: $FlowFixMe,
renderLanes: Lanes,
) {
resetSuspendedCurrentOnMountInLegacyMode(_current, workInProgress);
const props = workInProgress.pendingProps;
let context;
if (!disableLegacyContext) {
const unmaskedContext = getUnmaskedContext(
workInProgress,
Component,
false,
);
context = getMaskedContext(workInProgress, unmaskedContext);
}
prepareToReadContext(workInProgress, renderLanes);
let value;
let hasId;
// ~ これは関数コンポーネントを実行し、子要素を返します
value = renderWithHooks(
null,
workInProgress,
Component,
props,
context,
renderLanes,
);
hasId = checkDidRenderIdHook();
// React DevToolsはこのフラグを読み取ります
workInProgress.flags |= PerformedWork;
if (
// プロダクションではフラグがオフの場合のみチェック
!disableModulePatternComponents &&
typeof value === 'object' &&
value !== null &&
typeof value.render === 'function' &&
value.$$typeof === undefined
) {
// クラスインスタンスと仮定して処理
// ~ レンダリング後はIndeterminateComponentではなくなります
workInProgress.tag = ClassComponent;
...
} else {
// 関数コンポーネントと仮定して処理
// ~ レンダリング後はIndeterminateComponentではなくなります
workInProgress.tag = FunctionComponent;
if (getIsHydrating() && hasId) {
pushMaterializedTreeId(workInProgress);
}
// ~ currentがnullのため、mountChildFibers()が使用されます
reconcileChildren(null, workInProgress, value, renderLanes);
return workInProgress.child;
}
}
前述の通り、<App/>
のレンダリング時には HostRoot とは異なり current(以前のバージョン)が存在しないため、mountChildFibers()
が使用されます。またplaceSingleChild()
は挿入フラグを無視します。
App()
は<div>
を返し、これは後続のbeginWork()
でHostComponent
ブランチによって処理されます。
3.11 updateHostComponent()
function updateHostComponent(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
pushHostContext(workInProgress);
if (current === null) {
// ~ ハイドレーションについては別記事参照
tryToClaimNextHydratableInstance(workInProgress);
}
const type = workInProgress.type;
// ~ pendingPropsは<div/>の子要素(<p/>)を保持
const nextProps = workInProgress.pendingProps;
const prevProps = current !== null ? current.memoizedProps : null;
let nextChildren = nextProps.children;
// ~ 子要素が静的テキストの場合の最適化(例: <a/>)
const isDirectTextChild = shouldSetTextContent(type, nextProps);
if (isDirectTextChild) {
// ホストノードの直接のテキスト子要素を特別扱い
// 一般的なケースのため、通常の子要素として処理せず
// ホスト環境側で処理(別のHostText fiber作成を回避)
nextChildren = null;
} else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {
// 直接テキスト子要素から通常の子要素に切り替わる場合
// テキストコンテンツのリセットをスケジュール
workInProgress.flags |= ContentReset;
}
...
markRef(current, workInProgress);
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
上記のプロセスが<p/>
に対しても繰り返されますが、nextChildren
が配列になる点が異なります。
そのためreconcileChildFibers()
内部でreconcileChildrenArray()
が呼び出されます。
reconcileChildrenArray()
はkey
の存在により若干複雑になります。詳細は別記事
How does 'key' work internally? List diffing in React
を参照してください。
key
処理以外では基本的に最初の子 fiber を返し処理を継続します。兄弟要素は後で処理されます。
React がツリー構造をリンクリストにフラット化するためです。詳細は
How does React traverse Fiber tree internally
を参照してください。
<Link/>
では<App/>
と同様のプロセスを繰り返します。
<a>
と<button>
はテキストノードまで処理が進みますが、違いがあります。
<a>
は静的テキストを子要素に持ち、<button>
は JSX 式{count}
を含みます。
これが上記コードで<a>
のnextChildren
が null になり、<button>
が子要素処理を継続する理由です。
3.12 updateHostText()
<button>
の子要素は配列["click me - ", "0"]
ですが、updateHostText()
はbeginWork()
内で両方のテキストノードを処理するブランチです。
function updateHostText(current, workInProgress) {
if (current === null) {
tryToClaimNextHydratableInstance(workInProgress);
} // ここでは何も行いません。終端ノードです。完了ステップは直後に行われます
return null;
}
しかし、この関数はハイドレーション処理以外何も行いません。<a>
と<button>
のテキスト処理はコミットフェーズで行われます。
3.13 DOM ノードはcompleteWork()
でオフスクリーン作成される
How does React traverse Fiber tree internallyで説明した通り、
completeWork()
は兄弟要素のbeginWork()
処理前に呼び出されます。
Fiber Node には重要なプロパティstateNode
があり、HTML タグの場合実際の DOM ノードを参照します。
DOM ノードの実際の作成はcompleteWork()
で行われます。
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
// 注: ハイドレーション状態のチェックを意図的に行いません
// 現在のツリーとの比較は同等の速度でエラーが少ないため
// 理想的にはハイドレーション専用のワークループが必要
popTreeContext(workInProgress);
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
case ContextConsumer:
case MemoComponent:
bubbleProperties(workInProgress);
return null;
case ClassComponent: {
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
popLegacyContext(workInProgress);
}
bubbleProperties(workInProgress);
return null;
}
case HostRoot: {
...
return null;
}
...
// ~ For HTML tags
case HostComponent: {
popHostContext(workInProgress);
const type = workInProgress.type;
// ~ If there is current version, then we go to update branch
if (current !== null && workInProgress.stateNode != null) {
updateHostComponent(
current,
workInProgress,
type,
newProps,
renderLanes,
);
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
// ~ But we don't have current version yet, so we go to this mount branch
} else {
...
if (wasHydrated) {
...
} else {
const rootContainerInstance = getRootHostContainer();
// ~ 実際のDOMノード
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
// ~ 重要なポイント:DOMノード作成時には
// ~ サブツリーの直接接続されたDOMノード全ての親となる必要がある
appendAllChildren(instance, workInProgress, false, false);
workInProgress.stateNode = instance;
if (
// ~ これは後ほど説明します
finalizeInitialChildren(
instance,
type,
newProps,
rootContainerInstance,
currentHostContext,
)
) {
markUpdate(workInProgress);
}
}
if (workInProgress.ref !== null) {
// ホストノードにrefがある場合、コールバックをスケジュールする必要がある
markRef(workInProgress);
}
}
bubbleProperties(workInProgress);
...
return null;
}
case HostText: {
const newText = newProps;
if (current && workInProgress.stateNode != null) {
const oldText = current.memoizedProps;
// alternateが存在する場合、これはupdateを意味し、
// 更新を行うための副作用をスケジュールする必要がある
updateHostText(current, workInProgress, oldText, newText);
} else {
...
const rootContainerInstance = getRootHostContainer();
const currentHostContext = getHostContext();
const wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
if (prepareToHydrateHostTextInstance(workInProgress)) {
markUpdate(workInProgress);
}
} else {
workInProgress.stateNode = createTextInstance(
newText,
rootContainerInstance,
currentHostContext,
workInProgress,
);
}
}
bubbleProperties(workInProgress);
return null;
}
...
}
}
4. コミットフェーズにおける初期マウント
現時点で以下の状態が整っています:
- workInProgress バージョンの Fiber ツリーが最終的に構築されました!
- 基盤となる DOM ノードも作成・整理されました!
- DOM 操作をガイドするためのフラグが必要な Fiber に設定されました!
実際に React がどのように DOM を操作するかを見ていきましょう。
4.1 commitMutationEffects()
React 内部の概要で簡単に説明したコミットフェーズについて、
ここでは DOM の変更を処理するcommitMutationEffects()
を深く掘り下げます。
export function commitMutationEffects(
root: FiberRoot,
// ~ HostRootのFiber Node(新しく構築されたFiberツリーを保持)
finishedWork: Fiber,
committedLanes: Lanes
) {
inProgressLanes = committedLanes;
inProgressRoot = root;
commitMutationEffectsOnFiber(finishedWork, root, committedLanes);
inProgressLanes = null;
inProgressRoot = null;
}
function commitMutationEffectsOnFiber(
finishedWork: Fiber,
root: FiberRoot,
lanes: Lanes,
) {
const current = finishedWork.alternate;
const flags = finishedWork.flags;
// エフェクトフラグはfiberタイプを特定した*後*にチェックする必要があります
// fiberタグがより具体的なためです。例外はreconciliation関連のフラグで、
// これらは全てのfiberタイプに設定可能です
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
// ~ この再帰的呼び出しでサブツリーを先に処理します
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
// ~ ReconciliationEffectsはInsertionなどを意味します
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;
}
}
}
function recursivelyTraverseMutationEffects(
root: FiberRoot,
parentFiber: Fiber,
lanes: Lanes
) {
// Deletions effectsは任意のfiber typeにスケジュール可能です
// これらは子要素のeffectsが発火する前に処理される必要があります
// ~ Deletionは異なる方法で処理されます
const deletions = parentFiber.deletions;
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const childToDelete = deletions[i];
try {
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);
}
4.2 commitReconciliationEffects
commitReconciliationEffects()
は挿入(Insertion)や再配置などを処理します。
function commitReconciliationEffects(finishedWork: Fiber) {
// Placement effects(挿入、再配置)は任意のfiberタイプにスケジュール可能です
// これらは子要素のエフェクト発火後、自要素のエフェクト発火前に処理される必要があります
const flags = finishedWork.flags;
// ~ ここでフラグがチェックされます!
if (flags & Placement) {
try {
commitPlacement(finishedWork);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
// componentDidMountのようなライフサイクルが呼ばれる前に
// 挿入済みであることを示すため"placement"フラグをクリア
// TODO: findDOMNodeはこれに依存しなくなったがisMountedは依然使用
// ただしisMountedは非推奨のため削除可能
finishedWork.flags &= ~Placement;
}
...
}
デモケースでは、<App/>
の Fiber Node が実際にコミットされます。
4.3 commitPlacement()
function commitPlacement(finishedWork: Fiber): void {
...
// 再帰的に全てのホストノードを親要素に挿入
const parentFiber = getHostParentFiber(finishedWork);
// ~ 親fiberのタイプをチェック(挿入操作は親ノードに対して行われるため)
switch (parentFiber.tag) {
case HostSingleton: {
if (enableHostSingletons && supportsSingletons) {
const parent: Instance = parentFiber.stateNode;
const before = getHostSibling(finishedWork);
// 挿入されたトップレベルのFiberから開始し、
// 全ての終端ノードを見つけるために子要素を再帰処理
insertOrAppendPlacementNode(finishedWork, before, parent);
break;
}
// Fall through
}
// ~ 初期マウントではこのブランチは使用されない
case HostComponent: {
const parent: Instance = parentFiber.stateNode;
if (parentFiber.flags & ContentReset) {
// 挿入前に親要素のテキストコンテンツをリセット
resetTextContent(parent);
// ContentResetフラグをクリア
parentFiber.flags &= ~ContentReset;
}
const before = getHostSibling(finishedWork);
// 挿入された一番上のFiberのみを持っていますが、その子要素を再帰的に処理して
// 全ての終端ノードを見つける必要があります
insertOrAppendPlacementNode(finishedWork, before, parent);
break;
}
// ~ 初期マウントでPlacementフラグを持つ<App/>の親はHostRoot
case HostRoot:
case HostPortal: {
// ~ HostRootのstateNodeはFiberRootNodeを参照
const parent: Container = parentFiber.stateNode.containerInfo;
const before = getHostSibling(finishedWork);
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
break;
}
default:
throw new Error(
'Invalid host parent fiber. This error is likely caused by a bug ' +
'in React. Please file an issue.',
);
}
}
この処理は、finishedWork
の DOM を親 container の適切な位置に挿入または追加するというものです。
function insertOrAppendPlacementNodeIntoContainer(
node: Fiber,
before: ?Instance,
parent: Container
): void {
const { tag } = node;
const isHost = tag === HostComponent || tag === HostText;
if (isHost) {
const stateNode = node.stateNode;
// ~ DOM elementsの場合、直接挿入
if (before) {
insertInContainerBefore(parent, stateNode, before);
} else {
appendChildToContainer(parent, stateNode);
}
} else if (
tag === HostPortal ||
(enableHostSingletons && supportsSingletons ? tag === HostSingleton : false)
) {
// HostPortal自体が挿入対象の場合、子要素を再帰処理せず
// 各子要素から直接挿入処理を受け取ります
// HostSingletonの場合も独立して配置されます
} else {
// ~ 非DOM elementsの場合、子要素を再帰的に処理
const child = node.child;
if (child !== null) {
insertOrAppendPlacementNodeIntoContainer(child, before, parent);
let sibling = child.sibling;
while (sibling !== null) {
insertOrAppendPlacementNodeIntoContainer(sibling, before, parent);
sibling = sibling.sibling;
}
}
}
}
これが最終的に DOM が挿入される仕組みです。
5. まとめ
以上が、DOM が作成されコンテナに挿入される仕組みです。初期マウント時には以下の流れで処理されます:
- Fiber Tree は reconciliation 中に遅延生成され、基盤となる DOM ノードが同時に作成・整理されます
HostRoot
の直接の子要素にPlacement
フラグがマークされます- コミットフェーズでは
Placement
フラグを持つ fiber を検出し、その親が HostRoot であるため、DOM ノードをコンテナに挿入します