下面是一段React应用初始化时的代码,
import { createRoot } from 'react-dom/client';
document.body.innerHTML = '<div id="app"></div>';
const root = createRoot(document.getElementById('app'));
root.render(<h1>Hello, world</h1>);
下面我们将解析一下React初始化过程中几个比较重要的函数
一、入口:updateContainer
在执行完render 函数之后,react 内部会执行一个叫 updateContainer的函数,updateContainer
函数的主要作用是更新 React 容器,将新的 React 元素渲染到指定的容器中,并返回更新所使用的优先级通道(Lane)。
具体来说,它执行以下操作:
- 获取当前 Fiber 节点:
从传入的container
对象中获取当前的 Fiber 节点current
,这个节点代表了当前容器对应的 Fiber 树的根节点。 - 请求更新通道:
调用requestUpdateLane
函数,根据当前的 Fiber 节点current
来请求一个更新通道lane
。这个通道表示此次更新的优先级。 - 调用更新实现函数:
调用updateContainerImpl
函数,将current
、lane
、新的 React 元素element
、容器container
、父组件parentComponent
以及回调函数callback
作为参数传递给它,进行实际的更新操作。 - 返回更新通道:
返回请求到的更新通道lane
。 以下是updateContainer
函数的代码:
function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): Lane {
const current = container.current;
const lane = requestUpdateLane(current);
updateContainerImpl(
current,
lane,
element,
container,
parentComponent,
callback,
);
return lane;
}
二、scheduleUpdateOnFiber
之后会调用react-reconciler中一个重要的函数scheduleUpdateOnFiber
,具体来说,scheduleUpdateOnFiber
函数会将一个更新任务添加到 root
的更新队列中,然后根据 lane
的优先级来决定何时执行这个更新任务。这个函数是 React 协调器(Reconciler)的一部分,负责管理组件的更新和渲染过程。以下是该函数的主要实现:
export function scheduleUpdateOnFiber(
root: FiberRoot,
fiber: Fiber,
lane: Lane,
) {
if (__DEV__) {
if (isRunningInsertionEffect) {
console.error('useInsertionEffect must not schedule updates.');
}
}
if (__DEV__) {
if (isFlushingPassiveEffects) {
didScheduleUpdateDuringPassiveEffects = true;
}
}
// 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 {
// This is a normal update, scheduled from outside the render phase. For
// example, during an input event.
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
addFiberToLanesMap(root, fiber, lane);
}
}
warnIfUpdatesNotWrappedWithActDEV(fiber);
if (enableTransitionTracing) {
const transition = ReactSharedInternals.T;
if (transition !== null && transition.name != null) {
if (transition.startTime === -1) {
transition.startTime = now();
}
// $FlowFixMe[prop-missing]: The BatchConfigTransition and Transition types are incompatible but was previously untyped and thus uncaught
// $FlowFixMe[incompatible-call]: "
addTransitionToLanesMap(root, transition, lane);
}
}
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,
);
}
}
ensureRootIsScheduled(root);
if (
lane === SyncLane &&
executionContext === NoContext &&
!disableLegacyMode &&
(fiber.mode & ConcurrentMode) === NoMode
) {
if (__DEV__ && ReactSharedInternals.isBatchingLegacy) {
// Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.
} else {
// Flush the synchronous work now, unless we're already working or inside
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
// scheduleCallbackForFiber to preserve the ability to schedule a callback
// without immediately flushing it. We only do this for user-initiated
// updates, to preserve historical behavior of legacy mode.
resetRenderTimer();
flushSyncWorkOnLegacyRootsOnly();
}
}
}
}
三、 注册调度任务
scheduleUpdateOnFiber
函数之后, 立即进入ensureRootIsScheduled
函,在这个地方会与调度中心(scheduler
包)交互, 注册调度任务task
, 等待任务回调
export function ensureRootIsScheduled(root: FiberRoot): void {
// This function is called whenever a root receives an update. It does two
// things 1) it ensures the root is in the root schedule, and 2) it ensures
// there's a pending microtask to process the root schedule.
//
// Most of the actual scheduling logic does not happen until
// `scheduleTaskForRootDuringMicrotask` runs.
// 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;
// At the end of the current event, go through each of the roots and ensure
// there's a task scheduled for each one at the correct priority.
if (__DEV__ && ReactSharedInternals.actQueue !== null) {
// We're inside an `act` scope.
if (!didScheduleMicrotask_act) {
didScheduleMicrotask_act = true;
scheduleImmediateRootScheduleTask();
}
} else {
if (!didScheduleMicrotask) {
didScheduleMicrotask = true;
scheduleImmediateRootScheduleTask();
}
}
if (
__DEV__ &&
!disableLegacyMode &&
ReactSharedInternals.isBatchingLegacy &&
root.tag === LegacyRoot
) {
// Special `act` case: Record whenever a legacy update is scheduled.
ReactSharedInternals.didScheduleLegacyUpdate = true;
}
}
四、执行任务回调
任务回调, 实际上就是执行performSyncWorkOnRoot 简单看一下它们的源码,将主要逻辑剥离出来, 单个函数的代码量并不多.
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;
}
if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
syncNestedUpdateFlag();
}
const forceSync = true;
performWorkOnRoot(root, lanes, forceSync);
}
五、输出到用户界面
在输出阶段,commitRoot
其主要逻辑是处理副作用队列, 将最新的 fiber 树结构反映到 DOM 上.
function commitRoot(
root: FiberRoot,
finishedWork: null | Fiber,
lanes: Lanes,
recoverableErrors: null | Array<CapturedValue<mixed>>,
transitions: Array<Transition> | 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 !== NO_PENDING_EFFECTS);
flushRenderPhaseStrictModeWarningsInDEV();
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
throw new Error('Should not already be working.');
}
if (enableProfilerTimer && enableComponentPerformanceTrack) {
// Log the previous render phase once we commit. I.e. we weren't interrupted.
setCurrentTrackFromLanes(lanes);
if (exitStatus === RootErrored) {
logErroredRenderPhase(
completedRenderStartTime,
completedRenderEndTime,
lanes,
);
} else if (recoverableErrors !== null) {
const hydrationFailed =
finishedWork !== null &&
finishedWork.alternate !== null &&
(finishedWork.alternate.memoizedState: RootState).isDehydrated &&
(finishedWork.flags & ForceClientRender) !== NoFlags;
logRecoveredRenderPhase(
completedRenderStartTime,
completedRenderEndTime,
lanes,
recoverableErrors,
hydrationFailed,
);
} else {
logRenderPhase(completedRenderStartTime, completedRenderEndTime, lanes);
}
}
if (enableSchedulingProfiler) {
markCommitStarted(lanes);
}
if (finishedWork === null) {
if (enableSchedulingProfiler) {
markCommitStopped();
}
return;
} else {
if (__DEV__) {
if (lanes === NoLanes) {
console.error(
'finishedLanes should not be empty during a commit. This is a ' +
'bug in React.',
);
}
}
}
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);
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 (enableProfilerTimer) {
pendingEffectsRenderEndTime = completedRenderEndTime;
pendingSuspendedCommitReason = suspendedCommitReason;
}
// 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 (
// If this subtree rendered with profiling this commit, we need to visit it to log it.
(enableProfilerTimer &&
enableComponentPerformanceTrack &&
finishedWork.actualDuration !== 0) ||
(finishedWork.subtreeFlags & passiveSubtreeMask) !== NoFlags ||
(finishedWork.flags & passiveSubtreeMask) !== NoFlags
) {
if (enableYieldingBeforePassive) {
// We don't schedule a separate task for flushing passive effects.
// Instead, we just rely on ensureRootIsScheduled below to schedule
// a callback for us to flush the passive effects.
} else {
// So we can clear these now to allow a new callback to be scheduled.
root.callbackNode = null;
root.callbackPriority = NoLane;
scheduleCallback(NormalSchedulerPriority, () => {
if (enableProfilerTimer && enableComponentPerformanceTrack) {
// Track the currently executing event if there is one so we can ignore this
// event when logging events.
trackSchedulerEvent();
}
flushPassiveEffects(true);
// This render triggered passive effects: release the root cache pool
// *after* passive effects fire to avoid freeing a cache pool that may
// be referenced by a node in the tree (HostRoot, Cache boundary etc)
return null;
});
}
} else {
// 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;
}
if (enableProfilerTimer) {
// Mark the current commit time to be shared by all Profilers in this
// batch. This enables them to be grouped later.
resetCommitErrors();
recordCommitTime();
if (enableComponentPerformanceTrack) {
if (suspendedCommitReason === SUSPENDED_COMMIT) {
logSuspendedCommitPhase(completedRenderEndTime, commitStartTime);
} else if (suspendedCommitReason === THROTTLED_COMMIT) {
logSuspenseThrottlePhase(completedRenderEndTime, commitStartTime);
}
}
}
// 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 {
// 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;
}
}
pendingEffectsStatus = PENDING_MUTATION_PHASE;
const startedViewTransition =
enableViewTransition &&
shouldStartViewTransition &&
startViewTransition(
root.containerInfo,
pendingTransitionTypes,
flushMutationEffects,
flushLayoutEffects,
flushAfterMutationEffects,
flushSpawnedWork,
flushPassiveEffects,
);
if (!startedViewTransition) {
// Flush synchronously.
flushMutationEffects();
flushLayoutEffects();
// Skip flushAfterMutationEffects
flushSpawnedWork();
}
}
总的来说,commitRoot
函数是 React 更新过程中的一个关键环节,它负责将虚拟 DOM 树转换为真实的 DOM 树,并将更新后的内容展示给用户。