03|从 ensureRootIsScheduled 到 commitRoot:React 工作循环(WorkLoop)全景
本栏目是「React 源码剖析」系列:我会以源码为证据、以架构为线索,讲清 React 从构建、运行时到生态边界的关键设计。开源仓库:https://github.com/facebook/react
引言:这一篇在整个库里扮演什么角色?
前两篇我们把"入口层"打通了:
createRoot创建 Rootroot.render触发updateContainerupdateContainerImpl最终调用scheduleUpdateOnFiber
这一篇我们正式进入 React 的"发动机舱":
- Root Scheduler(根调度器):负责把"root 有活干"变成"排一个任务(task/microtask)去干"
- WorkLoop(工作循环):负责在 render/commit 两大阶段里把工作一步步推进,并在需要时 yield
你会看到 React 的核心设计主张:
- 把渲染变成可调度的工作(work),而不是一次函数调用
- 把副作用(DOM/生命周期/effects)集中在 commit,并拆成多个子阶段
- 把所有"何时做"统一交给调度层(Lane + Scheduler)
本文引用的主要源码文件:
packages/react-reconciler/src/ReactFiberWorkLoop.jspackages/react-reconciler/src/ReactFiberRootScheduler.js
核心概念:读 WorkLoop 必须先对齐的 6 个关键词
1) Lane / Lanes
Lane:一个优先级(位图中的某一位)Lanes:多个 lane 的集合(位图)
WorkLoop 的大多数决策,本质都是在回答:下一次渲染要处理哪些 lanes?
2) Root schedule vs Scheduler task
- Root schedule:React 自己维护的一张"有活的 root 链表"(可以多 root)
- Scheduler task:交给
scheduler包执行的任务(时间切片、优先级)
3) Microtask:为什么调度要延迟到 microtask 里做?
因为在一个浏览器事件里可能会发生很多 setState;React 希望把这些更新先收集,然后在 microtask 里统一决定:
- 哪些 root 需要 work
- 每个 root 用什么优先级
- 哪些同步 work 可以直接在 microtask 末尾 flush
4) Render phase vs Commit phase
- Render:纯计算(构建/比较 Fiber 树),允许被打断
- Commit:副作用(DOM 变更、layout effect、passive effect),需要更强的原子性
5) workInProgress & workInProgressRoot
WorkLoop 是"当前正在处理的 Fiber"在推进:
workInProgressRoot:当前 rootworkInProgress:当前 fiber
6) pendingEffectsStatus
Commit 不是"一口气做完",而是拆成多个状态:
PENDING_MUTATION_PHASEPENDING_LAYOUT_PHASEPENDING_AFTER_MUTATION_PHASEPENDING_PASSIVE_PHASE
这套状态机让 React 能把复杂副作用流程拆开,并能在某些场景(比如 view transition)里"半路暂停/继续"。
源码依次解析(Code Walkthrough):从一次更新到一次提交
下面按真实调用链拆解。
Step 1:scheduleUpdateOnFiber:把"更新"变成"root 上的 pending lanes"
文件:packages/react-reconciler/src/ReactFiberWorkLoop.js
js
export function scheduleUpdateOnFiber(
root: FiberRoot,
fiber: Fiber,
lane: Lane,
) {
// ...
// Mark that the root has a pending update.
markRootUpdated(root, lane);
// ...
ensureRootIsScheduled(root);
if (
lane === SyncLane &&
executionContext === NoContext &&
!disableLegacyMode &&
(fiber.mode & ConcurrentMode) === NoMode
) {
resetRenderTimer();
flushSyncWorkOnLegacyRootsOnly();
}
}
逐块解读:
markRootUpdated(root, lane):- 这是"数据面"的核心:root 上的
pendingLanes更新了。
- 这是"数据面"的核心:root 上的
ensureRootIsScheduled(root):- 这是"控制面"的核心:把这个 root 加入调度表,并保证稍后会有任务来处理它。
- 最下面 legacy 的 sync 分支:
- 历史行为兼容:非 concurrent mode 下,SyncLane 会尝试立刻 flush。
Trade-off:为什么 scheduleUpdateOnFiber 不直接 performWorkOnRoot?
因为 React 要允许:
- 同一事件内多个更新合并
- 根据 lane 决定优先级
- 并发渲染可中断
换句话说:scheduleUpdateOnFiber 只负责"登记",不负责"施工"。
Step 2:ensureRootIsScheduled:把 root 放进链表 + 安排 microtask
文件:packages/react-reconciler/src/ReactFiberRootScheduler.js
js
export let firstScheduledRoot: FiberRoot | null = null;
let lastScheduledRoot: FiberRoot | null = null;
let didScheduleMicrotask: boolean = false;
export function ensureRootIsScheduled(root: FiberRoot): void {
// 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;
}
}
mightHavePendingSyncWork = true;
ensureScheduleIsScheduled();
}
js
export function ensureScheduleIsScheduled(): void {
if (!didScheduleMicrotask) {
didScheduleMicrotask = true;
scheduleImmediateRootScheduleTask();
}
}
逐行解读:
firstScheduledRoot/lastScheduledRoot:- 用链表维护"所有有活的 root"。单 root 是常态,但 React 支持 multi root。
root.next !== null的快速路径:- 避免重复入队。
ensureScheduleIsScheduled():- 关键点:不是立刻调度 work,而是保证一个 microtask 会来处理 schedule。
设计动机:为什么用 microtask?
- 一个事件里可能多次
setState - React 希望批量决策,减少重复调度成本
这是非常典型的"事件末尾统一结算"策略。
Step 3:processRootScheduleInMicrotask:在 microtask 里决定每个 root 的任务优先级
仍在 ReactFiberRootScheduler.js
js
function processRootScheduleInMicrotask() {
didScheduleMicrotask = false;
mightHavePendingSyncWork = false;
const currentTime = now();
let prev = null;
let root = firstScheduledRoot;
while (root !== null) {
const next = root.next;
const nextLanes = scheduleTaskForRootDuringMicrotask(root, currentTime);
if (nextLanes === NoLane) {
root.next = null;
if (prev === null) {
firstScheduledRoot = next;
} else {
prev.next = next;
}
if (next === null) {
lastScheduledRoot = prev;
}
} else {
prev = root;
if (includesSyncLane(nextLanes) || (enableGestureTransition && isGestureRender(nextLanes))) {
mightHavePendingSyncWork = true;
}
}
root = next;
}
if (!hasPendingCommitEffects()) {
flushSyncWorkAcrossRoots_impl(syncTransitionLanes, false);
}
}
这段代码你可以当成"根调度器的主循环"。
关键点:
- 对每个 root 调
scheduleTaskForRootDuringMicrotask:- 决定它 next lanes
- 要不要安排 Scheduler task
- 是否可以把同步任务留给 microtask 尾巴直接 flush
nextLanes === NoLane时会把 root 从链表移除:- 只允许在 microtask 中移除,避免 reentrancy bug
Trade-off:为什么"移除 root"只能在 microtask 做?
因为渲染/commit 过程中可能又会 schedule update;如果你允许随时移除,会出现链表损坏或漏调度这种"非常难查"的 bug。
Step 4:scheduleTaskForRootDuringMicrotask:同步 lane 不走 Scheduler,直接等 microtask flush
文件:ReactFiberRootScheduler.js
js
function scheduleTaskForRootDuringMicrotask(
root: FiberRoot,
currentTime: number,
): Lane {
markStarvedLanesAsExpired(root, currentTime);
const rootWithPendingPassiveEffects = getRootWithPendingPassiveEffects();
const pendingPassiveEffectsLanes = getPendingPassiveEffectsLanes();
const workInProgressRoot = getWorkInProgressRoot();
const workInProgressRootRenderLanes = getWorkInProgressRootRenderLanes();
const rootHasPendingCommit =
root.cancelPendingCommit !== null || root.timeoutHandle !== noTimeout;
const nextLanes =
enableYieldingBeforePassive && root === rootWithPendingPassiveEffects
? pendingPassiveEffectsLanes
: getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
rootHasPendingCommit,
);
const existingCallbackNode = root.callbackNode;
if (nextLanes === NoLanes || (root === workInProgressRoot && isWorkLoopSuspendedOnData()) || root.cancelPendingCommit !== null) {
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode);
}
root.callbackNode = null;
root.callbackPriority = NoLane;
return NoLane;
}
if (includesSyncLane(nextLanes) && !checkIfRootIsPrerendering(root, nextLanes)) {
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode);
}
root.callbackPriority = SyncLane;
root.callbackNode = null;
return SyncLane;
}
// 否则:根据 lanesToEventPriority 映射成 Scheduler priority
// Scheduler_scheduleCallback(... performWorkOnRootViaSchedulerTask)
}
逐块解读:
markStarvedLanesAsExpired:- 防止低优先级饿死(starvation)。过期 lane 会被提升处理。
getNextLanes(...):- "lane 选择器"是调度器的灵魂:下一次渲染做哪些 lanes。
includesSyncLane(nextLanes):- 同步 lane 不需要另起 Scheduler task
- 因为 microtask 末尾会
flushSyncWorkAcrossRoots_impl(...)
这就是 React 在现代版本里的一次重大变化:
- Sync work 倾向于在 microtask 中 flush
- 而不是靠 Scheduler 的 ImmediatePriority
这样能减少调度层级,也更贴近浏览器事件语义。
Step 5:performWorkOnRoot:进入 WorkLoop,决定 concurrent 还是 sync 渲染
文件:ReactFiberWorkLoop.js
js
export function performWorkOnRoot(
root: FiberRoot,
lanes: Lanes,
forceSync: boolean,
): void {
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
throw new Error('Should not already be working.');
}
const shouldTimeSlice =
(!forceSync &&
!includesBlockingLane(lanes) &&
!includesExpiredLane(root, lanes)) ||
checkIfRootIsPrerendering(root, lanes);
let exitStatus: RootExitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes, true);
// ... 处理一致性检查、错误恢复、finishConcurrentRender
ensureRootIsScheduled(root);
}
解读:
shouldTimeSlice:决定是否启用时间切片(concurrent work loop)。renderRootConcurrentvsrenderRootSync:- 两条路径最终都构建出
finishedWork(root.current.alternate) - 区别在于 concurrent 可以 yield,sync 不 yield
- 两条路径最终都构建出
Trade-off:为什么不是"所有更新都并发"?
- 过期 lane 不能再让步,否则会饿死
- blocking lane(例如某些用户交互)可能要求更快完成
所以 WorkLoop 的第一件事就是平衡:响应性 vs 吞吐量。
Step 6:renderRootConcurrent:可中断渲染的核心循环
文件:ReactFiberWorkLoop.js
js
function renderRootConcurrent(root: FiberRoot, lanes: Lanes): RootExitStatus {
const prevExecutionContext = executionContext;
executionContext |= RenderContext;
// 如果 root 或 lanes 变了,prepareFreshStack
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
resetRenderTimer();
prepareFreshStack(root, lanes);
}
outer: do {
try {
// ... suspended reason 的处理
if (__DEV__ && ReactSharedInternals.actQueue !== null) {
workLoopSync();
} else if (enableThrottledScheduling) {
workLoopConcurrent(includesNonIdleWork(lanes));
} else {
workLoopConcurrentByScheduler();
}
break;
} catch (thrownValue) {
handleThrow(root, thrownValue);
}
} while (true);
// workInProgress 是否为 null 决定是否完成
}
其中真正"推进 Fiber"的是:
js
function workLoopConcurrentByScheduler() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
而 performUnitOfWork 把 begin/complete 串起来:
js
function performUnitOfWork(unitOfWork: Fiber): void {
const current = unitOfWork.alternate;
let next = beginWork(current, unitOfWork, entangledRenderLanes);
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
}
解读:
shouldYield()来自 Scheduler:- 这是 React 并发能力的"阀门"------什么时候把控制权还给浏览器。
beginWork/completeWork分别是"向下走"和"向上归并"。
Trade-off:为什么 render 要拆成 begin/complete?
- begin 阶段负责生成/复用 child fibers(拓展树)
- complete 阶段负责把子树结果归并到父节点(收口)
这是典型的 DFS 两阶段遍历,能让 React:
- 以 fiber 为粒度中断
- 恢复时从 workInProgress 继续
Step 7:commitRoot:把副作用拆成多个子阶段
文件:ReactFiberWorkLoop.js
commit 中最关键的一段"阶段拆分"是:
js
// The commit phase is broken into several sub-phases.
// ... before mutation -> mutation -> layout -> (after mutation) -> passive
const subtreeHasBeforeMutationEffects =
(finishedWork.subtreeFlags & (BeforeMutationMask | MutationMask)) !== NoFlags;
if (subtreeHasBeforeMutationEffects || rootHasBeforeMutationEffect) {
executionContext |= CommitContext;
try {
commitBeforeMutationEffects(root, finishedWork, lanes);
} finally {
executionContext = prevExecutionContext;
}
}
pendingEffectsStatus = PENDING_MUTATION_PHASE;
flushMutationEffects();
flushLayoutEffects();
flushSpawnedWork();
flushMutationEffects 做两件非常关键的事:
js
commitMutationEffects(root, finishedWork, lanes);
// The work-in-progress tree is now the current tree.
root.current = finishedWork;
pendingEffectsStatus = PENDING_LAYOUT_PHASE;
解读:
- BeforeMutation :读 host tree(典型:
getSnapshotBeforeUpdate) - Mutation:真正改 DOM
- 在 Mutation 之后立刻
root.current = finishedWork:- 因为 layout effects / lifecycle 需要看到"最新的树"
最后,flushPassiveEffects 把 passive effects 延后执行:
js
commitPassiveUnmountEffects(root.current);
commitPassiveMountEffects(root, root.current, lanes, transitions, pendingEffectsRenderEndTime);
Trade-off:为什么 passive effects 通常要延后?
useEffect不应阻塞用户看到更新- 允许浏览器先 paint,再跑 effect
但 React 也有一个"例外":如果这次 commit 包含 sync lane,可能会在 commit 尾部立刻 flush(为了让结果可立即观察)。
一图看懂:Root Scheduler + WorkLoop 的整体流程
SyncLane
Non-Sync
Yes
No
scheduleUpdateOnFiber
markRootUpdated
ensureRootIsScheduled
根链表 firstScheduledRoot/lastScheduledRoot
安排 microtask
processRootScheduleInMicrotask
scheduleTaskForRootDuringMicrotask
microtask 末尾 flushSyncWorkAcrossRoots
Scheduler_scheduleCallback
performWorkOnRootViaSchedulerTask
performSyncWorkOnRoot
performWorkOnRoot
shouldTimeSlice?
renderRootConcurrent
renderRootSync
finishConcurrentRender
commitRoot / commitRootWhenReady
BeforeMutation
Mutation - root.current = finishedWork
Layout
Passive - 通常 after paint
总结:这一篇你应该带走的 5 个设计思想
scheduleUpdateOnFiber是登记处,不是施工队
它只负责把 lane 记到 root 上,并触发根调度器。
- React 自己维护"root schedule",再把执行交给 Scheduler
这层让 React 能更自由地实现 microtask batching、multi-root、以及对特殊阶段(passive/commit)的处理。
- microtask 是现代 React 的关键节拍器
- 事件内聚合更新
- microtask 里统一决策
- microtask 尾部 flush sync work
- WorkLoop 的本质是可恢复的 DFS
workInProgress 指针 + beginWork/completeWork 两阶段,保证随时可中断、可恢复。
- Commit 是一个分阶段的状态机,不是一坨"副作用"
BeforeMutation / Mutation / Layout / Passive 拆开,分别满足不同的语义要求与性能目标。
下一篇预告
第 4 篇我们会把这一章里多次出现但尚未深入的主角拉出来单讲:
Lane位图优先级如何影响getNextLanesentangledRenderLanes/Transitions 为什么需要"纠缠(entangle)"includesExpiredLane、markStarvedLanesAsExpired如何避免饿死