Skip to main content

コミットの仕組み

コミットは初回のレンダー(root.render())または更新時のレンダーが終了した時点で発生します。最初のレンダーからの流れをまず説明します。

コンカレントレンダーの終了処理

アプリケーションの初期レンダーとこれまでのメカニズムが完了した後、React は以下を呼び出します:

finishConcurrentRender(root, exitStatus, finishedWork, lanes);

関数シグネチャ

finishConcurrentRender の定義:

function finishConcurrentRender(
root: FiberRoot,
exitStatus: RootExitStatus,
finishedWork: Fiber,
lanes: Lanes
) {
// [...code]
commitRootWhenReady(
root,
finishedWork,
workInProgressRootRecoverableErrors,
workInProgressTransitions,
lanes
);
// [...code]
}

実装ステップ

  1. 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;
    }
    }
  2. root が suspended 状態でリトライ時のみの場合:

    if (
    includesOnlyRetries(lanes) &&
    (alwaysThrottleRetries || exitStatus === RootSuspended)
    ) {
    // ...
    }
    1. タイムアウトまでのミリ秒を計算し、10 ミリ秒未満は無視:
      const msUntilTimeout =
      globalMostRecentFallbackTime + FALLBACK_THROTTLE_MS - now();
      if (msUntilTimeout > 10) {
      // ...
      }
    2. nextLanesを計算し、作業がなければ関数を終了(このパスはレンダーが suspended され、リトライ時のみ):
      const nextLanes = getNextLanes(root, NoLanes);
      if (nextLanes !== NoLanes) {
      return;
      }
    3. setTimeoutでタイムアウトをスケジュールし、準備ができたら root をコミット:
      root.timeoutHandle = scheduleTimeout(
      commitRootWhenReady.bind(
      null,
      root,
      finishedWork,
      workInProgressRootRecoverableErrors,
      workInProgressTransitions,
      lanes
      ),
      msUntilTimeout
      );
      return;
      これにより Suspense フォールバックを表示するために root がコミットされます 😉
  3. それ以外の場合(root が suspended されておらず正常に完了)、即座にcommitRootWhenReadyを呼び出し:

    commitRootWhenReady(
    root,
    finishedWork,
    workInProgressRootRecoverableErrors,
    workInProgressTransitions,
    lanes
    );

準備完了時のコミット処理の仕組み

実際のコミットは後述するcommitRoot関数で発生しますが、コンカレントレンダーの場合は常にcommitRootWhenReadyから呼び出されます。performSyncWorkOnRootからの同期レンダーは独立してcommitRootを呼び出します。

関数シグネチャ

commitRootWhenReadyの定義:

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は以下の処理を実行します:

  1. 削除エフェクト: 削除されたコンポーネントのクリーンアップを実行
  2. Reconciliation エフェクト: 新しい DOM ノードを正しい位置に挿入
  3. 更新エフェクト: DOM ノードを現在のレンダー値で更新
  4. 挿入エフェクトのクリーンアップ: useInsertionEffectのクリーンアップ
  5. 挿入エフェクト: useInsertionEffectの実行
  6. レイアウトエフェクトのクリーンアップ: 詳細はAndrew のコメントを参照
tip

注意点:

  • Layout と passive effects は下から上へ実行
  • Layout と passive effects のクリーンアップは上から下へ実行
  • クリーンアップは実際のエフェクト実行前に一括処理(上から下へクリーンアップ後、下から上へエフェクト実行)
  • useInsertionEffectは layout/passive effects とは異なる順序
  • useInsertionEffectのクリーンアップとエフェクトは各コンポーネントで下から上へ実行

3. Layout effects

Layout effectsは同期的に実行され、ブラウザのメインスレッドをブロックします。

処理内容はコンポーネントタイプによって異なります:

  • 関数コンポーネント: useLayoutEffect
  • クラスコンポーネント: componentDidMountcomponentDidUpdate
  • Ref のアタッチ

4. Passive effects

SyncLaneでのレンダー時は passive effects も同期的に実行されます(一般的ではありません)。

SyncLaneでのレンダー時は、commitRootImplでスケジュールされた通りに非同期実行されます。