[리액트 19] 4편. Reconciler 동작 흐름
4편. Reconciler 동작 흐름
지금까지 거시적인 패키지 구조와 주요 두 작업루프에 대한 설명을 통해 react-reconciler 패키지에 대해 어느 정도 이해했습니다.
4편에서는 react-reconciler 패키지의 주요 역할을 정리하며 주요 기능들을 4가지로 나눠서 설명하겠습니다.
입력: schedulerUpdateOnFiber와 같은 API 함수를 외부에 노출하여, react 패키지 등 다른 패키지에서 호출할 수 있도록 합니다.
스케줄링 Task 등록: scheduler 패키지와 상호작용하여 task를 등록하고 콜백을 기다립니다.
task 콜백 실행: fiber 트리를 구성하고, 동시에 렌더러(react-dom 또는 react-native)와 상호작용하여 fiber에 대응하는 DOM 노드를 생성합니다.
출력: 렌더러와 상호작용하여 실제 DOM 노드를 렌더링합니다. (커밋)
위 1~4번 과정을 그림에서 나타냈습니다. (ReactFiberWorkLoop.js, ReactFiberRootScheduler.js)
ReactFiberWorkLoop.js에서 입력을 담당하는 sccheduleUpdateOnFiber 뿐입니다. react-reconciler 패키지에서 외부에 노출되는 api 함수들 중에서 fiber를 변경해야 하는 작업이 포함된다면 모두다 최종적으로는 scheduleUpdateOnFiber를 호출하게 됩니다. 따라서 scheduleUpdateOnFiber 함수는 입력 흐름에서 반드시 거쳐야 하는 경로입니다.
export function scheduleUpdateOnFiber(
root: FiberRoot,
fiber: Fiber,
lane: Lane,
) {
// Check if the work loop is currently suspended and waiting for data to
// finish loading.
if (
// Suspended render phase
(root === workInProgressRoot &&
(workInProgressSuspendedReason === SuspendedOnData ||
workInProgressSuspendedReason === SuspendedOnAction)) ||
// Suspended commit phase
root.cancelPendingCommit !== null
) {
// The incoming update might unblock the current render. Interrupt the
// current attempt and restart from the top.
prepareFreshStack(root, NoLanes);
const didAttemptEntireTree = false;
markRootSuspended(
root,
workInProgressRootRenderLanes,
workInProgressDeferredLane,
didAttemptEntireTree,
);
}
// Mark that the root has a pending update.
markRootUpdated(root, lane);
if (
(executionContext & RenderContext) !== NoLanes &&
root === workInProgressRoot
) {
// This update was dispatched during the render phase. This is a mistake
// if the update originates from user space (with the exception of local
// hook updates, which are handled differently and don't reach this
// function), but there are some internal React features that use this as
// an implementation detail, like selective hydration.
warnAboutRenderPhaseUpdatesInDEV(fiber);
// Track lanes that were updated during the render phase
workInProgressRootRenderPhaseUpdatedLanes = mergeLanes(
workInProgressRootRenderPhaseUpdatedLanes,
lane,
);
} else {
if (root === workInProgressRoot) {
// Received an update to a tree that's in the middle of rendering. Mark
// that there was an interleaved update work on this root.
if ((executionContext & RenderContext) === NoContext) {
workInProgressRootInterleavedUpdatedLanes = mergeLanes(
workInProgressRootInterleavedUpdatedLanes,
lane,
);
}
if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
// The root already suspended with a delay, which means this render
// definitely won't finish. Since we have a new update, let's mark it as
// suspended now, right before marking the incoming update. This has the
// effect of interrupting the current render and switching to the update.
// TODO: Make sure this doesn't override pings that happen while we've
// already started rendering.
const didAttemptEntireTree = false;
markRootSuspended(
root,
workInProgressRootRenderLanes,
workInProgressDeferredLane,
didAttemptEntireTree,
);
}
}
// 스케줄러(Scheduler) 패키지를 통한 스케줄링 작업 등록,
// 간접적으로 `fiber 구성` 수행
ensureRootIsScheduled(root);
}
scheduleUpdateOnFiber 함수로 진입한 이후 항상 ensureRootIsScheduled가 실행되는 것을 알 수 있습니다. (React 18버전 이전에는 SyncLane인지 확인한 후에 스케줄링을 거치지 않고 직접 fiber를 구성하는 함수가 있었습니다. 하지만 현재는 모든 업데이트가 Scheduler를 거쳐서 fiber를 구성합니다.)
위 코드 예시를 보면 scheduleUpdateOnFiber 함수 실행 후 ensureRootIsScheduled 함수를 실행합니다.
// 중복된 마이크로태스크가 예약되는 것을 방지하기 위해 사용됩니다.
let didScheduleMicrotask: boolean = false;
// 동기화 작업(sync work)이 없으면 flushSync에서 빠르게 탈출하기 위해 사용됩니다.(
let mightHavePendingSyncWork: boolean = false;
export function ensureRootIsScheduled(root: FiberRoot): void {
// 이 함수는 root가 update를 받을때마다 항상 실행됩니다. 이 함수는 2가지 기능을 수행합니다.
// 1) root가 root schedule에 포함되어 있는지 확인합니다.
// 2) root schedule을 처리하기 위한 pending microtask가 있는지 확인합니다.
//
// 실제 스케줄링 로직의 대부분은 `scheduleTaskForRootDuringMicrotask`가 실행될 때 수행됩니다.
// Add the root to the schedule
if (root === lastScheduledRoot || root.next !== null) {
// Fast path. This root is already scheduled.
} else {
if (lastScheduledRoot === null) {
firstScheduledRoot = lastScheduledRoot = root;
} else {
lastScheduledRoot.next = root;
lastScheduledRoot = root;
}
}
// Any time a root received an update, we set this to true until the next time
// we process the schedule. If it's false, then we can quickly exit flushSync
// without consulting the schedule.
mightHavePendingSyncWork = true;
if (!didScheduleMicrotask) {
didScheduleMicrotask = true;
scheduleImmediateRootScheduleTask();
}
}
function scheduleImmediateRootScheduleTask() {
// TODO: Can we land supportsMicrotasks? Which environments don't support it?
// Alternatively, can we move this check to the host config?
if (supportsMicrotasks) {
scheduleMicrotask(() => {
// In Safari, appending an iframe forces microtasks to run.
// https://github.com/facebook/react/issues/22459
// We don't support running callbacks in the middle of render
// or commit so we need to check against that.
const executionContext = getExecutionContext();
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
// Note that this would still prematurely flush the callbacks
// if this happens outside render or commit phase (e.g. in an event).
// Intentionally using a macrotask instead of a microtask here. This is
// wrong semantically but it prevents an infinite loop. The bug is
// Safari's, not ours, so we just do our best to not crash even though
// the behavior isn't completely correct.
Scheduler_scheduleCallback(
ImmediateSchedulerPriority,
processRootScheduleInImmediateTask,
);
return;
}
// 처리할 작업이 없으면 실행.
processRootScheduleInMicrotask();
});
} else {
// micarotask가 지원되지 않는 브라우저나 환경에서 scheduler 바로 예약
Scheduler_scheduleCallback(
ImmediateSchedulerPriority,
processRootScheduleInImmediateTask,
);
}
}
ensureRootIsScheduled는 scheduled되었는지 확인하고 scheduleImmediateRootScheduleTask를 통해 Scheduler로 task가 예약되는 것을 확인할 수 있습니다.
task 콜백은 performWorkOnRoot를 통해 실행됩니다. 이 함수의 파라미터 값 중에 forceSync 값이 true이면 performSyncWorkOnRoot를 실행하고, false면 performConcurrentWorkOnRoot 함수를 실행합니다.
이 단계에서는 아래와 같이 3가지 과정을 거칩니다.
shouldTimeSlice(Concurrent 여부) 체크 후 Fiber 트리 구성.
renderRootSync일 경우 renderRootSync 함수(React19에서는 Legacy 호환용으로 거의 사용 X)
renderRootSync는 렌더링 중간에 중단이 불가능합니다.
remderRootConcurrent일 경우 renderRootConcurrent 힘수
renderRootConcurrent는 렌더링 중간에 중단이 가능하며, 렌더링 과정 중인지, 이전 렌더링을 복구해야 하는지 확인합니다.
중간에 중단되었다면, performWorkOnRootViaSchedulerTask 함수를 다시 스케줄링하여 이어서 처리합니다.
예외 처리(Suspense일 경우 해당 상태로 변경하여 출력시 반영)
performWorkOnRoot => finishConcurrentRender => commitRootWhenReady => commitRoot 함수로 출력 (commit)
performWorkOnRoot
export function performWorkOnRoot(
root: FiberRoot,
lanes: Lanes,
forceSync: boolean,
): void {
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
throw new Error('Should not already be working.');
}
// We disable time-slicing in some cases: if the work has been CPU-bound
// for too long ("expired" work, to prevent starvation), or we're in
// sync-updates-by-default mode.
const shouldTimeSlice =
(!forceSync &&
!includesBlockingLane(lanes) &&
!includesExpiredLane(root, lanes)) ||
// If we're prerendering, then we should use the concurrent work loop
// even if the lanes are synchronous, so that prerendering never blocks
// the main thread.
// TODO: We should consider doing this whenever a sync lane is suspended,
// even for regular pings.
(enableSiblingPrerendering && checkIfRootIsPrerendering(root, lanes));
//
//
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes, true);
let renderWasConcurrent = shouldTimeSlice;
//
do {
if (exitStatus === RootInProgress) {
// Render phase is still in progress.
if (
enableSiblingPrerendering &&
workInProgressRootIsPrerendering &&
!shouldTimeSlice
) {
// We're in prerendering mode, but time slicing is not enabled. This
// happens when something suspends during a synchronous update. Exit the
// the work loop. When we resume, we'll use the concurrent work loop so
// that prerendering is non-blocking.
//
// Mark the root as suspended. Usually we do this at the end of the
// render phase, but we do it here so that we resume in
// prerendering mode.
// TODO: Consider always calling markRootSuspended immediately.
// Needs to be we attach a ping listener, though.
const didAttemptEntireTree = false;
markRootSuspended(root, lanes, NoLane, didAttemptEntireTree);
}
break;
} else {
// The render completed.
// Check if this render may have yielded to a concurrent event, and if so,
// confirm that any newly rendered stores are consistent.
// TODO: It's possible that even a concurrent render may never have yielded
// to the main thread, if it was fast enough, or if it expired. We could
// skip the consistency check in that case, too.
const finishedWork: Fiber = (root.current.alternate: any);
if (
renderWasConcurrent &&
!isRenderConsistentWithExternalStores(finishedWork)
) {
if (enableProfilerTimer && enableComponentPerformanceTrack) {
setCurrentTrackFromLanes(lanes);
logInconsistentRender(renderStartTime, renderEndTime);
finalizeRender(lanes, renderEndTime);
}
// A store was mutated in an interleaved event. Render again,
// synchronously, to block further mutations.
exitStatus = renderRootSync(root, lanes, false);
// We assume the tree is now consistent because we didn't yield to any
// concurrent events.
renderWasConcurrent = false;
// Need to check the exit status again.
continue;
}
// 에러 발생했는지 확인
if (
(disableLegacyMode || root.tag !== LegacyRoot) &&
exitStatus === RootErrored
) {
const lanesThatJustErrored = lanes;
const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
root,
lanesThatJustErrored,
);
if (errorRetryLanes !== NoLanes) {
if (enableProfilerTimer && enableComponentPerformanceTrack) {
setCurrentTrackFromLanes(lanes);
logErroredRenderPhase(renderStartTime, renderEndTime, lanes);
finalizeRender(lanes, renderEndTime);
}
lanes = errorRetryLanes;
exitStatus = recoverFromConcurrentError(
root,
lanesThatJustErrored,
errorRetryLanes,
);
renderWasConcurrent = false;
// Need to check the exit status again.
if (exitStatus !== RootErrored) {
// The root did not error this time. Restart the exit algorithm
// from the beginning.
// TODO: Refactor the exit algorithm to be less confusing. Maybe
// more branches + recursion instead of a loop. I think the only
// thing that causes it to be a loop is the RootSuspendedAtTheShell
// check. If that's true, then we don't need a loop/recursion
// at all.
continue;
}
}
}
if (exitStatus === RootFatalErrored) {
prepareFreshStack(root, NoLanes);
// Since this is a fatal error, we're going to pretend we attempted
// the entire tree, to avoid scheduling a prerender.
const didAttemptEntireTree = true;
markRootSuspended(root, lanes, NoLane, didAttemptEntireTree);
break;
}
// We now have a consistent tree. The next step is either to commit it,
// or, if something suspended, wait to commit it after a timeout.
//
finishConcurrentRender(
root,
exitStatus,
finishedWork,
lanes,
renderEndTime,
);
}
break;
} while (true);
// 종료 전에 다시 확인: 다른 업데이트가 있는지, 새 스케줄이 필요한지.
ensureRootIsScheduled(root);
}
performSyncWorkOnRoot, performWorkOnRootViaSchedulerTask
function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes) {
// This is the entry point for synchronous tasks that don't go
// through Scheduler.
const didFlushPassiveEffects = flushPendingEffects();
if (didFlushPassiveEffects) {
// If passive effects were flushed, exit to the outer work loop in the root
// scheduler, so we can recompute the priority.
return null;
}
const forceSync = true;
performWorkOnRoot(root, lanes, forceSync);
}
// SchedulerTask를 통해 work
function performWorkOnRootViaSchedulerTask(
root: FiberRoot,
didTimeout: boolean,
): RenderTaskFn | null {
// This is the entry point for concurrent tasks scheduled via Scheduler (and
// postTask, in the future).
if (hasPendingCommitEffects()) {
// We are currently in the middle of an async committing (such as a View Transition).
// We could force these to flush eagerly but it's better to defer any work until
// it finishes. This may not be the same root as we're waiting on.
// TODO: This relies on the commit eventually calling ensureRootIsScheduled which
// always calls processRootScheduleInMicrotask which in turn always loops through
// all the roots to figure out. This is all a bit inefficient and if optimized
// it'll need to consider rescheduling a task for any skipped roots.
root.callbackNode = null;
root.callbackPriority = NoLane;
return null;
}
// Flush any pending passive effects before deciding which lanes to work on,
// in case they schedule additional work.
const originalCallbackNode = root.callbackNode;
const didFlushPassiveEffects = flushPendingEffects(true);
if (didFlushPassiveEffects) {
// Something in the passive effect phase may have canceled the current task.
// Check if the task node for this root was changed.
if (root.callbackNode !== originalCallbackNode) {
// The current task was canceled. Exit. We don't need to call
// `ensureRootIsScheduled` because the check above implies either that
// there's a new task, or that there's no remaining work on this root.
return null;
} else {
// Current task was not canceled. Continue.
}
}
// Determine the next lanes to work on, using the fields stored on the root.
// TODO: We already called getNextLanes when we scheduled the callback; we
// should be able to avoid calling it again by stashing the result on the
// root object. However, because we always schedule the callback during
// a microtask (scheduleTaskForRootDuringMicrotask), it's possible that
// an update was scheduled earlier during this same browser task (and
// therefore before the microtasks have run). That's because Scheduler batches
// together multiple callbacks into a single browser macrotask, without
// yielding to microtasks in between. We should probably change this to align
// with the postTask behavior (and literally use postTask when
// it's available).
const workInProgressRoot = getWorkInProgressRoot();
const workInProgressRootRenderLanes = getWorkInProgressRootRenderLanes();
const rootHasPendingCommit =
root.cancelPendingCommit !== null || root.timeoutHandle !== noTimeout;
const lanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
rootHasPendingCommit,
);
if (lanes === NoLanes) {
// No more work on this root.
return null;
}
// Enter the work loop.
// TODO: We only check `didTimeout` defensively, to account for a Scheduler
// bug we're still investigating. Once the bug in Scheduler is fixed,
// we can remove this, since we track expiration ourselves.
const forceSync = !disableSchedulerTimeoutInWorkLoop && didTimeout;
//performWorkOnRoot 실행.
performWorkOnRoot(root, lanes, forceSync);
// The work loop yielded, but there may or may not be work left at the current
// priority. Need to determine whether we need to schedule a continuation.
// Usually `scheduleTaskForRootDuringMicrotask` only runs inside a microtask;
// however, since most of the logic for determining if we need a continuation
// versus a new task is the same, we cheat a bit and call it here. This is
// only safe to do because we know we're at the end of the browser task.
// So although it's not an actual microtask, it might as well be.
scheduleTaskForRootDuringMicrotask(root, now());
if (root.callbackNode != null && root.callbackNode === originalCallbackNode) {
// The task node scheduled for this root is the same one that's
// currently executed. Need to return a continuation.
return performWorkOnRootViaSchedulerTask.bind(null, root);
}
return null;
}
출력 단계에서는 commitRoot 함수를 통해 Effetcs가 실행되고, 화면에 그려집니다. 크게 3단계로 나눠진다고 볼 수 있습니다.
commitBeforeMutationEffects
DOM 변경 이전 단계. fiber.flags가 Snapshot, Passive에 해당하는 경우 처리합니다.
commitMutationEffects
host tree(DOM 또는 RN)을 변경하는 단계. fiber.flags가 Placement, Update, Deletion, Hydrating에 해당하는 경우 처리합니다.
commitLayoutEffects
DOM 변경 이후, Effects(Side Effects)를 실행하는 단계입니다. fiber.flags가 Update, Callback인 경우 처리합니다.
ReactFiberFlags.js FiberWorkLoop.js(commitRoot)
finishConcurrentRender, commitRootWhenReady
function finishConcurrentRender(
root: FiberRoot,
exitStatus: RootExitStatus,
finishedWork: Fiber,
lanes: Lanes,
renderEndTime: number, // Profiling-only
) {
// TODO: The fact that most of these branches are identical suggests that some
// of the exit statuses are not best modeled as exit statuses and should be
// tracked orthogonally.
switch (exitStatus) {
case RootInProgress:
case RootFatalErrored: {
throw new Error('Root did not complete. This is a bug in React.');
}
case RootSuspendedWithDelay: {
if (!includesOnlyTransitions(lanes)) {
// Commit the placeholder.
break;
}
}
// Fallthrough
case RootSuspendedAtTheShell: {
const didAttemptEntireTree = !workInProgressRootDidSkipSuspendedSiblings;
markRootSuspended(
root,
lanes,
workInProgressDeferredLane,
didAttemptEntireTree,
);
return;
}
case RootErrored: {
// This render errored. Ignore any recoverable errors because we weren't actually
// able to recover. Instead, whatever the final errors were is the ones we log.
// This ensures that we only log the actual client side error if it's just a plain
// error thrown from a component on the server and the client.
workInProgressRootRecoverableErrors = null;
break;
}
case RootSuspended:
case RootCompleted: {
break;
}
default: {
throw new Error('Unknown root exit status.');
}
}
if (
includesOnlyRetries(lanes) &&
(alwaysThrottleRetries || exitStatus === RootSuspended)
) {
// This render only included retries, no updates. Throttle committing
// retries so that we don't show too many loading states too quickly.
const msUntilTimeout =
globalMostRecentFallbackTime + FALLBACKMS
// Don't bother with a very short suspense time.
if (msUntilTimeout > 10) {
const didAttemptEntireTree =
!workInProgressRootDidSkipSuspendedSiblings;
markRootSuspended(
root,
lanes,
workInProgressDeferredLane,
didAttemptEntireTree,
);
const nextLanes = getNextLanes(root, NoLanes, true);
if (nextLanes !== NoLanes) {
// There's additional work we can do on this root. We might as well
// attempt to work on that while we're suspended.
return;
}
// The render is suspended, it hasn't timed out, and there's no
// lower priority work to do. Instead of committing the fallback
// immediately, wait for more data to arrive.
// TODO: Combine retry throttling with Suspensey commits. Right now they
// run one after the other.
root.timeoutHandle = scheduleTimeout(
commitRootWhenReady.bind(
null,
root,
finishedWork,
workInProgressRootRecoverableErrors,
workInProgressTransitions,
workInProgressRootDidIncludeRecursiveRenderUpdate,
lanes,
workInProgressDeferredLane,
workInProgressRootInterleavedUpdatedLanes,
workInProgressSuspendedRetryLanes,
workInProgressRootDidSkipSuspendedSiblings,
exitStatus,
THROTTLED_COMMIT,
renderStartTime,
renderEndTime,
),
msUntilTimeout,
);
return;
}
}
// 출력: Fiber 트리를 기반으로 실제 렌더링(Coomit).
commitRootWhenReady(
root,
finishedWork,
workInProgressRootRecoverableErrors,
workInProgressTransitions,
workInProgressRootDidIncludeRecursiveRenderUpdate,
lanes,
workInProgressDeferredLane,
workInProgressRootInterleavedUpdatedLanes,
workInProgressSuspendedRetryLanes,
workInProgressRootDidSkipSuspendedSiblings,
exitStatus,
IMMEDIATE_COMMIT,
renderStartTime,
renderEndTime,
);
}
function commitRootWhenReady(
root: FiberRoot,
finishedWork: Fiber,
recoverableErrors: Array> | null,
transitions: Array | null,
didIncludeRenderPhaseUpdate: boolean,
lanes: Lanes,
spawnedLane: Lane,
updatedLanes: Lanes,
suspendedRetryLanes: Lanes,
didSkipSuspendedSiblings: boolean,
exitStatus: RootExitStatus,
suspendedCommitReason: SuspendedCommitReason, // Profiling-only
completedRenderStartTime: number, // Profiling-only
completedRenderEndTime: number, // Profiling-only
) {
root.timeoutHandle = noTimeout;
// TODO: Combine retry throttling with Suspensey commits. Right now they run
// one after the other.
const BothVisibilityAndMaySuspendCommit = Visibility | MaySuspendCommit;
const subtreeFlags = finishedWork.subtreeFlags;
const isViewTransitionEligible =
enableViewTransition && includesOnlyViewTransitionEligibleLanes(lanes); // TODO: Use a subtreeFlag to optimize.
const isGestureTransition = enableSwipeTransition && isGestureRender(lanes);
const maySuspendCommit =
subtreeFlags & ShouldSuspendCommit ||
(subtreeFlags & BothVisibilityAndMaySuspendCommit) ===
BothVisibilityAndMaySuspendCommit;
if (isViewTransitionEligible || maySuspendCommit || isGestureTransition) {
// Before committing, ask the renderer whether the host tree is ready.
// If it's not, we'll wait until it notifies us.
startSuspendingCommit();
// This will walk the completed fiber tree and attach listeners to all
// the suspensey resources. The renderer is responsible for accumulating
// all the load events. This all happens in a single synchronous
// transaction, so it track state in its own module scope.
// This will also track any newly added or appearing ViewTransition
// components for the purposes of forming pairs.
accumulateSuspenseyCommit(finishedWork);
if (isViewTransitionEligible || isGestureTransition) {
// If we're stopping gestures we don't have to wait for any pending
// view transition. We'll stop it when we commit.
if (!enableSwipeTransition || root.stoppingGestures === null) {
suspendOnActiveViewTransition(root.containerInfo);
}
}
// At the end, ask the renderer if it's ready to commit, or if we should
// suspend. If it's not ready, it will return a callback to subscribe to
// a ready event.
const schedulePendingCommit = waitForCommitToBeReady();
if (schedulePendingCommit !== null) {
// NOTE: waitForCommitToBeReady returns a subscribe function so that we
// only allocate a function if the commit isn't ready yet. The other
// pattern would be to always pass a callback to waitForCommitToBeReady.
// Not yet ready to commit. Delay the commit until the renderer notifies
// us that it's ready. This will be canceled if we start work on the
// root again.
root.cancelPendingCommit = schedulePendingCommit(
commitRoot.bind(
null,
root,
finishedWork,
lanes,
recoverableErrors,
transitions,
didIncludeRenderPhaseUpdate,
spawnedLane,
updatedLanes,
suspendedRetryLanes,
exitStatus,
SUSPENDED_COMMIT,
completedRenderStartTime,
completedRenderEndTime,
),
);
const didAttemptEntireTree = !didSkipSuspendedSiblings;
markRootSuspended(root, lanes, spawnedLane, didAttemptEntireTree);
return;
}
}
// Otherwise, commit immediately.;
commitRoot(
root,
finishedWork,
lanes,
recoverableErrors,
transitions,
didIncludeRenderPhaseUpdate,
spawnedLane,
updatedLanes,
suspendedRetryLanes,
exitStatus,
suspendedCommitReason,
completedRenderStartTime,
completedRenderEndTime,
);
}
commitRoot
function commitRoot(
root: FiberRoot,
finishedWork: null | Fiber,
lanes: Lanes,
recoverableErrors: null | Array>,
transitions: Array | null,
didIncludeRenderPhaseUpdate: boolean,
spawnedLane: Lane,
updatedLanes: Lanes,
suspendedRetryLanes: Lanes,
exitStatus: RootExitStatus,
suspendedCommitReason: SuspendedCommitReason, // Profiling-only
completedRenderStartTime: number, // Profiling-only
completedRenderEndTime: number, // Profiling-only
): void {
root.cancelPendingCommit = null;
do {
// `flushPassiveEffects` will call `flushSyncUpdateQueue` at the end, which
// means `flushPassiveEffects` will sometimes result in additional
// passive effects. So we need to keep flushing in a loop until there are
// no more pending effects.
// TODO: Might be better if `flushPassiveEffects` did not automatically
// flush synchronous work at the end, to avoid factoring hazards like this.
flushPendingEffects();
} while (pendingEffectsStatus !== NOEFFECTS);
flushRenderPhaseStrictModeWarningsInDEV();
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
throw new Error('Should not already be working.');
}
if (finishedWork === null) {
if (enableSwipeTransition) {
// Stop any gestures that were completed and is now being reverted.
if (root.stoppingGestures !== null) {
stopCompletedGestures(root);
}
}
return;
}
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.',
);
}
// Check which lanes no longer have any work scheduled on them, and mark
// those as finished.
let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);
// Make sure to account for lanes that were updated by a concurrent event
// during the render phase; don't mark them as finished.
const concurrentlyUpdatedLanes = getConcurrentlyUpdatedLanes();
remainingLanes = mergeLanes(remainingLanes, concurrentlyUpdatedLanes);
if (enableSwipeTransition && root.pendingGestures === null) {
// Gestures don't clear their lanes while the gesture is still active but it
// might not be scheduled to do any more renders and so we shouldn't schedule
// any more gesture lane work until a new gesture is scheduled.
remainingLanes &= ~GestureLane;
}
markRootFinished(
root,
lanes,
remainingLanes,
spawnedLane,
updatedLanes,
suspendedRetryLanes,
);
// Reset this before firing side effects so we can detect recursive updates.
didIncludeCommitPhaseUpdate = false;
if (root === workInProgressRoot) {
// We can reset these now that they are finished.
workInProgressRoot = null;
workInProgress = null;
workInProgressRootRenderLanes = NoLanes;
} else {
// This indicates that the last root we worked on is not the same one that
// we're committing now. This most commonly happens when a suspended root
// times out.
}
// workInProgressX might be overwritten, so we want
// to store it in pendingPassiveX until they get processed
// We need to pass this through as an argument to commitRoot
// because workInProgressX might have changed between
// the previous render and commit if we throttle the commit
// with setTimeout
pendingFinishedWork = finishedWork;
pendingEffectsRoot = root;
pendingEffectsLanes = lanes;
pendingEffectsRemainingLanes = remainingLanes;
pendingPassiveTransitions = transitions;
pendingRecoverableErrors = recoverableErrors;
pendingDidIncludeRenderPhaseUpdate = didIncludeRenderPhaseUpdate;
if (enableSwipeTransition && isGestureRender(lanes)) {
// This is a special kind of render that doesn't commit regular effects.
commitGestureOnRoot(
root,
finishedWork,
recoverableErrors,
enableProfilerTimer
? suspendedCommitReason === IMMEDIATE_COMMIT
? completedRenderEndTime
: commitStartTime
: 0,
);
return;
}
// If there are pending passive effects, schedule a callback to process them.
// Do this as early as possible, so it is queued before anything else that
// might get scheduled in the commit phase. (See #16714.)
// TODO: Delete all other places that schedule the passive effect callback
// They're redundant.
let passiveSubtreeMask;
if (enableViewTransition) {
pendingViewTransitionEvents = null;
if (includesOnlyViewTransitionEligibleLanes(lanes)) {
// Claim any pending Transition Types for this commit.
// This means that multiple roots committing independent View Transitions
// 1) end up staggered because we can only have one at a time.
// 2) only the first one gets all the Transition Types.
pendingTransitionTypes = ReactSharedInternals.V;
ReactSharedInternals.V = null;
passiveSubtreeMask = PassiveTransitionMask;
} else {
pendingTransitionTypes = null;
passiveSubtreeMask = PassiveMask;
}
} else {
passiveSubtreeMask = PassiveMask;
}
// If we don't have passive effects, we're not going to need to perform more work
// so we can clear the callback now.
root.callbackNode = null;
root.callbackPriority = NoLane;
resetShouldStartViewTransition();
// The commit phase is broken into several sub-phases. We do a separate pass
// of the effect list for each phase: all mutation effects come before all
// layout effects, and so on.
// Check if there are any effects in the whole tree.
// TODO: This is left over from the effect list implementation, where we had
// to check for the existence of `firstEffect` to satisfy Flow. I think the
// only other reason this optimization exists is because it affects profiling.
// Reconsider whether this is necessary.
const subtreeHasBeforeMutationEffects =
(finishedWork.subtreeFlags & (BeforeMutationMask | MutationMask)) !==
NoFlags;
const rootHasBeforeMutationEffect =
(finishedWork.flags & (BeforeMutationMask | MutationMask)) !== NoFlags;
if (subtreeHasBeforeMutationEffects || rootHasBeforeMutationEffect) {
const prevTransition = ReactSharedInternals.T;
ReactSharedInternals.T = null;
const previousPriority = getCurrentUpdatePriority();
setCurrentUpdatePriority(DiscreteEventPriority);
const prevExecutionContext = executionContext;
executionContext |= CommitContext;
try {
// 첫번째 "before mutaion" 단계.
// The first phase a "before mutation" phase. We use this phase to read the
// state of the host tree right before we mutate it. This is where
// getSnapshotBeforeUpdate is called.
commitBeforeMutationEffects(root, finishedWork, lanes);
} finally {
// Reset the priority to the previous non-sync value.
executionContext = prevExecutionContext;
setCurrentUpdatePriority(previousPriority);
ReactSharedInternals.T = prevTransition;
}
}
let willStartViewTransition = shouldStartViewTransition;
if (enableSwipeTransition) {
// Stop any gestures that were completed and is now being committed.
if (root.stoppingGestures !== null) {
stopCompletedGestures(root);
// If we are in the process of stopping some gesture we shouldn't start
// a View Transition because that would start from the previous state to
// the next state.
willStartViewTransition = false;
}
}
pendingEffectsStatus = PENDINGPHASE;
const startedViewTransition =
enableViewTransition &&
willStartViewTransition &&
startViewTransition(
root.containerInfo,
pendingTransitionTypes,
flushMutationEffects,
flushLayoutEffects,
flushAfterMutationEffects,
flushSpawnedWork,
flushPassiveEffects,
reportViewTransitionError,
);
if (!startedViewTransition) {
// Flush synchronously.
// 두번째 "mutaion" 단계. host tree(Dom 트리)를 변경하는 단계
flushMutationEffects();
// 세번째 "layout" 단계. host tree 변경 후 effects를 실행하는 단계
flushLayoutEffects();
// Skip flushAfterMutationEffects
flushSpawnedWork();
}
}
정리
이번 편에서는 reconciler의 동작 과정을 4단계로 나누어 거시적으로 분석했습니다. 한 문장으로 쉽게 요약하면 setState같은 함수로 입력이 일어나고, scheduler에 task로 등록이 되고, task가 실행되고, 마지막으로 commit을 통해 화면에 출력이 되며 Effects까지 실행됩니다. 다음 편에서는 React의 시작 과정에 대해 설명하도록 하겠습니다.
2025-04-30