コミットの仕組み
コミットは初回のレンダー(root.render()
)または更新時のレンダーが終了した時点で発生します。最初のレンダーからの流れをまず説明します。
コンカレントレンダーの終了処理
アプリケーションの初期レンダーとこれまでのメカニズムが完了した後、React は以下を呼び出します:
finishConcurrentRender(root, exitStatus, finishedWork, lanes);
関数シグネチャ
function finishConcurrentRender(
root: FiberRoot,
exitStatus: RootExitStatus,
finishedWork: Fiber,
lanes: Lanes
) {
// [...code]
commitRootWhenReady(
root,
finishedWork,
workInProgressRootRecoverableErrors,
workInProgressTransitions,
lanes
);
// [...code]
}
実装ステップ
exitStatus
による分岐処理:switch (existStatus) {
case RootInProgress:
case RootFatalErrored: {
// 防御策
throw new Error("Root did not complete. This is a bug in React.");
}
case RootErrored:
case RootSuspended:
case RootCompleted: {
// 関数の実行を継続
break;
}
default: {
throw new Error("Unknown root exit status.");
}
case RootSuspendedWithDelay: {
if (includesOnlyTransitions(lanes)) {
markRootSuspended(root, lanes);
// このケースではrootをsuspendedとマークして処理を終了
return;
}
break;
}
}root が suspended 状態でリトライ時のみの場合:
if (
includesOnlyRetries(lanes) &&
(alwaysThrottleRetries || exitStatus === RootSuspended)
) {
// ...
}- タイムアウトまでのミリ秒を計算し、10 ミリ秒未満は無視:
const msUntilTimeout =
globalMostRecentFallbackTime + FALLBACK_THROTTLE_MS - now();
if (msUntilTimeout > 10) {
// ...
} nextLanes
を計算し、作業がなければ関数を終了(このパスはレンダーが suspended され、リトライ時のみ):const nextLanes = getNextLanes(root, NoLanes);
if (nextLanes !== NoLanes) {
return;
}setTimeout
でタイムアウトをスケジュールし、準備ができたら root をコミット:これにより Suspense フォールバックを表示するために root がコミットされます 😉root.timeoutHandle = scheduleTimeout(
commitRootWhenReady.bind(
null,
root,
finishedWork,
workInProgressRootRecoverableErrors,
workInProgressTransitions,
lanes
),
msUntilTimeout
);
return;
- タイムアウトまでのミリ秒を計算し、10 ミリ秒未満は無視:
それ以外の場合(root が suspended されておらず正常に完了)、即座に
commitRootWhenReady
を呼び出し:commitRootWhenReady(
root,
finishedWork,
workInProgressRootRecoverableErrors,
workInProgressTransitions,
lanes
);
準備完了時のコミット処理の仕組み
実際のコミットは後述するcommitRoot
関数で発生しますが、コンカレントレンダーの場合は常にcommitRootWhenReady
から呼び出されます。performSyncWorkOnRoot
からの同期レンダーは独立してcommitRoot
を呼び出します。
関数シグネチャ
function commitRootWhenReady(
root: FiberRoot,
finishedWork: Fiber,
recoverableErrors: Array<CapturedValue<mixed>> | null,
transitions: Array<Transition> | null,
lanes: Lanes
) {
// ...
}
実装ステップ
この関数は、緊急レーンで呼び出された場合はすぐにcommitRoot
を呼び出します:
if (includesOnlyNonUrgentLanes(lanes)) {
// ... code
return;
}
commitRoot(root, recoverableErrors, transitions);
緊急レーンは以下の定義に従います。
const UrgentLanes = SyncLane | InputContinuousLane | DefaultLane;
return (lanes & UrgentLanes) === NoLanes;
SyncLane
InputContinuousLane
DefaultLane
root.render()
から発生する処理はDefaultLane
に属するため、緊急とみなされcommitRoot
が呼び出されます。
すべての lane が非緊急の場合、React はSuspensey commit
を実行し、コミットを後でスケジュールします:
if (includesOnlyNonUrgentLanes(lanes)) {
startSuspendingCommit();
accumulateSuspenseyCommit(finishedWork);
const schedulePendingCommit = waitForCommitToBeReady();
if (schedulePendingCommit !== null) {
root.cancelPendingCommit = schedulePendingCommit(
commitRoot.bind(null, root, recoverableErrors, transitions)
);
markRootSuspended(root, lanes);
return;
}
}
※ 簡潔さのため、上記コードの詳細な説明は割愛しています。詳細な解説が必要な場合はイシューをオープンしてください。
commitRoot
の仕組み
commitRoot
は以下のようにcommitRootImpl
を呼び出します:
function commitRoot(
root: FiberRoot,
recoverableErrors: null | Array<CapturedValue<mixed>>,
transitions: Array<Transition> | null
) {
const previousUpdateLanePriority = getCurrentUpdatePriority();
const prevTransition = ReactCurrentBatchConfig.transition;
try {
ReactCurrentBatchConfig.transition = null;
setCurrentUpdatePriority(DiscreteEventPriority);
// ハイライト-next-line
commitRootImpl(
root,
recoverableErrors,
transitions,
previousUpdateLanePriority
);
} finally {
ReactCurrentBatchConfig.transition = prevTransition;
setCurrentUpdatePriority(previousUpdateLanePriority);
}
return null;
}
commitRootImpl
関数は非常に長く複雑で、多くの他の関数や再帰処理を呼び出します。
これは非常に情報量が多く複雑なコードを含むセクションになるため、準備してください!
次に説明する内容の非常に簡略化されたバージョンは以下の通りです(詳細はdetails
タグ内に記載):
簡略化されたcommitRootImplの全体像
// 大幅に簡略化
function commitRootImpl(
root: FiberRoot,
recoverableErrors: null | Array<CapturedValue<mixed>>,
transitions: Array<Transition> | null,
renderPriorityLevel: EventPriority
) {
do {
// well, commitRoot may be triggerred while we have a scheduled pending
// effects processing.
// in this case, we need to pass over them now.
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
throw new Error("既に処理中であってはなりません");
}
const finishedWork = root.finishedWork;
const lanes = root.finishedLanes;
if (finishedWork === null) {
return null;
}
root.finishedWork = null;
root.finishedLanes = NoLanes;
if (finishedWork === root.current) {
throw new Error(
"Cannot commit the same tree as before. This error is likely caused by " +
"a bug in React. Please file an issue."
);
}
root.callbackNode = null;
root.callbackPriority = NoLane;
root.cancelPendingCommit = null;
let remainingLanes = mergeLanes(
finishedWork.lanes | finishedWork.childLanes,
getConcurrentlyUpdatedLanes()
);
markRootFinished(root, remainingLanes);
if (root === workInProgressRoot) {
workInProgressRoot = null;
workInProgress = null;
workInProgressRootRenderLanes = NoLanes;
}
if (
(finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
(finishedWork.flags & PassiveMask) !== NoFlags
) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
pendingPassiveEffectsRemainingLanes = remainingLanes;
pendingPassiveTransitions = transitions;
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}
const subtreeHasEffects =
(finishedWork.subtreeFlags &
(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
NoFlags;
const rootHasEffect =
(finishedWork.flags &
(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
NoFlags;
if (subtreeHasEffects || rootHasEffect) {
const prevTransition = ReactCurrentBatchConfig.transition;
ReactCurrentBatchConfig.transition = null;
const previousPriority = getCurrentUpdatePriority();
setCurrentUpdatePriority(DiscreteEventPriority);
const prevExecutionContext = executionContext;
executionContext |= CommitContext;
ReactCurrentOwner.current = null;
const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
root,
finishedWork
);
// The next phase is the mutation phase, where we mutate the host tree.
commitMutationEffects(root, finishedWork, lanes);
resetAfterCommit(root.containerInfo);
root.current = finishedWork;
commitLayoutEffects(finishedWork, root, lanes);
requestPaint();
executionContext = prevExecutionContext;
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig.transition = prevTransition;
} else {
// No effects.
root.current = finishedWork;
}
const rootDidHavePassiveEffects = rootDoesHavePassiveEffects;
if (rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = false;
rootWithPendingPassiveEffects = root;
pendingPassiveEffectsLanes = lanes;
} else {
releaseRootPooledCache(root, remainingLanes);
}
// Read this again, since an effect might have updated it
remainingLanes = root.pendingLanes;
if (remainingLanes === NoLanes) {
legacyErrorBoundariesThatAlreadyFailed = null;
}
ensureRootIsScheduled(root);
if (recoverableErrors !== null) {
// remember this? createRoot options 😉
const onRecoverableError = root.onRecoverableError;
for (let i = 0; i < recoverableErrors.length; i++) {
const recoverableError = recoverableErrors[i];
const errorInfo = makeErrorInfo(
recoverableError.digest,
recoverableError.stack
);
onRecoverableError(recoverableError.value, errorInfo);
}
}
if (hasUncaughtError) {
hasUncaughtError = false;
const error = firstUncaughtError;
firstUncaughtError = null;
throw error;
}
if (includesSyncLane(pendingPassiveEffectsLanes) && root.tag !== LegacyRoot) {
flushPassiveEffects();
}
// Read this again, since a passive effect might have updated it
remainingLanes = root.pendingLanes;
if (includesSyncLane(remainingLanes)) {
if (root === rootWithNestedUpdates) {
nestedUpdateCount++;
} else {
nestedUpdateCount = 0;
rootWithNestedUpdates = root;
}
} else {
nestedUpdateCount = 0;
}
flushSyncWorkOnAllRoots();
return null;
}
このステップの目的は、アプリケーションの結果を画面に表示するための複数のエフェクトステージを実行することです。
root プロパティのリセット
const finishedWork = root.finishedWork;
root.finishedWork = null;
root.finishedLanes = NoLanes;
root.callbackNode = null;
root.callbackPriority = NoLane;
root.cancelPendingCommit = null;
root.current = finishedWork;
if (root === workInProgressRoot) {
workInProgressRoot = null;
workInProgress = null;
workInProgressRootRenderLanes = NoLanes;
}
// ...後続処理...
Passive Effects のスケジューリング
React はツリーをスマートにタグ付けし、エフェクトの存在を検知します。エフェクトが存在する場合、後で処理するコールバックをスケジュールします:
if (
(finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
(finishedWork.flags & PassiveMask) !== NoFlags
) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
pendingPassiveEffectsRemainingLanes = remainingLanes;
pendingPassiveTransitions = transitions;
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}
このコールバックは非同期で実行され、React が他のタイプのエフェクトを処理する時間を確保します。
エフェクトの実行
React は複数のエフェクトタイプをサポートしており、ここでまとめて実行されます:
const subtreeHasEffects =
(finishedWork.subtreeFlags &
(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
NoFlags;
const rootHasEffect =
(finishedWork.flags &
(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
NoFlags;
if (rootHasEffect || subtreeHasEffects) {
const prevTransition = ReactCurrentBatchConfig.transition;
ReactCurrentBatchConfig.transition = null;
const previousPriority = getCurrentUpdatePriority();
setCurrentUpdatePriority(DiscreteEventPriority);
const prevExecutionContext = executionContext;
executionContext |= CommitContext;
ReactCurrentOwner.current = null;
commitBeforeMutationEffects(root, finishedWork);
commitMutationEffects(root, finishedWork, lanes);
resetAfterCommit(root.containerInfo);
root.current = finishedWork;
commitLayoutEffects(finishedWork, root, lanes);
requestPaint();
executionContext = prevExecutionContext;
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig.transition = prevTransition;
}
if (recoverableErrors !== null) {
// createRootのonRecoverableErrorオプション
callRootOnRecoverableErrors(root);
}
if (hasUncaughtError) {
hasUncaughtError = false;
const error = firstUncaughtError;
firstUncaughtError = null;
throw error;
}
if (includesSyncLane(pendingPassiveEffectsLanes) && root.tag !== LegacyRoot) {
flushPassiveEffects();
}
// [...] コミット処理の続き
React は以下のエフェクトを順番に実行します:
1. Before mutation effects
このエフェクトは、前のツリーを変更する前にライフサイクルイベントをトリガーします。
例えばクラスコンポーネントのgetSnapshotBeforeUpdate
や実験的なuseEffectEvent
の呼び出しに使用されます。
完全な処理はこちらで確認できます
2. Mutation effects
Mutation effectsは以下の処理を実行します:
- 削除エフェクト: 削除されたコンポーネントのクリーンアップを実行
- Reconciliation エフェクト: 新しい DOM ノードを正しい位置に挿入
- 更新エフェクト: DOM ノードを現在のレンダー値で更新
- 挿入エフェクトのクリーンアップ:
useInsertionEffect
のクリーンアップ - 挿入エフェクト:
useInsertionEffect
の実行 - レイアウトエフェクトのクリーンアップ: 詳細はAndrew のコメントを参照
注意点:
- Layout と passive effects は
下から上へ
実行 - Layout と passive effects のクリーンアップは
上から下へ
実行 - クリーンアップは実際のエフェクト実行前に一括処理(上から下へクリーンアップ後、下から上へエフェクト実行)
useInsertionEffect
は layout/passive effects とは異なる順序useInsertionEffect
のクリーンアップとエフェクトは各コンポーネントで下から上へ
実行
3. Layout effects
Layout effectsは同期的に実行され、ブラウザのメインスレッドをブロックします。
処理内容はコンポーネントタイプによって異なります:
- 関数コンポーネント:
useLayoutEffect
- クラスコンポーネント:
componentDidMount
、componentDidUpdate
- Ref のアタッチ
4. Passive effects
SyncLane
でのレンダー時は passive effects も同期的に実行されます(一般的ではありません)。
非SyncLane
でのレンダー時は、commitRootImpl
でスケジュールされた通りに非同期実行されます。