15|把“整套引擎”讲通:一条更新从触发到落地的全链路复盘
本文深入剖析了React从更新触发到最终落地的全链路机制。通过源码分析,揭示了React更新流程的核心设计: 入口阶段:ReactDOMRoot只是薄门面,将渲染请求转化为Reconciler的container update 更新标记:updateContainer创建更新并标记lane,通过enqueueUpdate加入队列 调度准备:scheduleUpdateOnFiber标记root的p
15|把“整套引擎”讲通:一条更新从触发到落地的全链路复盘
本栏目是「React 源码剖析」系列:我会以源码为证据、以架构为线索,讲清 React 从运行时到核心算法的关键设计。开源仓库:https://github.com/facebook/react
前一篇(第 14 篇)我们从 Hook 的实现视角,抓住了一个“连接点”:Hook 更新不是“重新执行组件函数”,而是往 queue 里追加 update,然后把这次更新接到 scheduleUpdateOnFiber 上。
这一篇是最后一篇,我们把镜头拉到更广:把这条链路从“入口”拉到“落地”,把你读到一半经常断开的几个环补上:
root.render(<App />)到底怎么变成 Fiber work?- lane/优先级是怎么决定“先做谁、做多久、能不能中断”的?
- render 完成后,为什么 commit 不是立刻发生?它为什么还要
commitRootWhenReady? - 最后一次更新,怎么真正落到 DOM(Host 层)?
本文核心文件(串联视角,按链路顺序):
packages/react-dom/src/client/ReactDOMRoot.jspackages/react-reconciler/src/ReactFiberReconciler.jspackages/react-reconciler/src/ReactFiberWorkLoop.jspackages/react-reconciler/src/ReactFiberRootScheduler.jspackages/react-reconciler/src/ReactFiberLane.js
0) 先给全链路一个“路线图”:入口只是 enqueue,真正开跑在 root scheduler
先给一张“你应该怎么在脑子里摆放这些模块”的路线图。
- ReactDOM(renderer 门面):把“我要渲染/更新/卸载”翻译成对 Reconciler 的调用。
- Reconciler(平台无关内核):维护 root、fiber tree、更新队列、lane;把更新调度到 WorkLoop。
- RootScheduler(root 维度调度器):把“root 上有哪些 lane 待处理”变成一个“宿主任务”(microtask / scheduler task),并在合适的时机调用
performWorkOnRoot。 - WorkLoop(render/commit 工作循环):真正的 render phase(reconcile/可中断)+ commit phase(副作用/确定性)。
- HostConfig/DOM bindings(平台落地):commit 阶段把 Host fiber 的副作用落到真实 DOM。
如果你只记一句话:
enqueueUpdate只是把请求挂起来;ensureRootIsScheduled才是让引擎“开始动起来”的总开关。
1) 从 root.render 开始:ReactDOMRoot 只是薄门面
入口非常直白,ReactDOMRoot.prototype.render 里几乎只做了一件事:
- 调
updateContainer(children, root, null, null)
源码(packages/react-dom/src/client/ReactDOMRoot.js):
function ReactDOMRoot(internalRoot: FiberRoot) {
this._internalRoot = internalRoot;
}
ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render =
function (children: ReactNodeList): void {
const root = this._internalRoot;
if (root === null) {
throw new Error('Cannot update an unmounted root.');
}
updateContainer(children, root, null, null);
};
你在这里应该形成一个“去魅认知”:
ReactDOMRoot不负责 Fiber、不负责调度、不负责 DOM diff。- 它只是在 ReactDOM 这一层把“渲染一个 element”翻译成 Reconciler 的一次 container update。
这也是 React 能做多端 renderer 的原因之一:入口“语义”一致,落地靠 HostConfig。
2) updateContainer:一次更新先变成 root fiber 的一次 Update
updateContainer 在 ReactFiberReconciler.js。最关键的几行是:
const lane = requestUpdateLane(current);const update = createUpdate(lane); update.payload = {element};const root = enqueueUpdate(rootFiber, update, lane);scheduleUpdateOnFiber(root, rootFiber, lane);
源码(packages/react-reconciler/src/ReactFiberReconciler.js):
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?component(...props: any),
callback: ?Function,
): Lane {
const current = container.current;
const lane = requestUpdateLane(current);
updateContainerImpl(
current,
lane,
element,
container,
parentComponent,
callback,
);
return lane;
}
function updateContainerImpl(
rootFiber: Fiber,
lane: Lane,
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?component(...props: any),
callback: ?Function,
): void {
const update = createUpdate(lane);
update.payload = {element};
const root = enqueueUpdate(rootFiber, update, lane);
if (root !== null) {
scheduleUpdateOnFiber(root, rootFiber, lane);
entangleTransitions(root, rootFiber, lane);
}
}
这里可以把第 14 篇的 Hook 更新链路对上:
- Hook:
dispatchSetState也会走requestUpdateLane,也会 enqueue,然后scheduleUpdateOnFiber。 - Root.render:只不过它 enqueue 的是“root element 更新”,而 Hook enqueue 的是“某个 hook queue 的 update”。
统一的地方在于:最后都要走到 scheduleUpdateOnFiber(root, fiber, lane)。
3) scheduleUpdateOnFiber:只做“标记与入队”,不直接 render
读 scheduleUpdateOnFiber 的关键,是不要把它误解成“开始 render”。它更多是:
- 标记 root 有 pending lanes:
markRootUpdated(root, lane) - 处理特殊状态(比如 render/commit 正在 suspend)
- 确保 root 被调度:
ensureRootIsScheduled(root)
源码(packages/react-reconciler/src/ReactFiberWorkLoop.js):
export function scheduleUpdateOnFiber(
root: FiberRoot,
fiber: Fiber,
lane: Lane,
) {
// ...省略 dev / passive effects 标记
// Mark that the root has a pending update.
markRootUpdated(root, lane);
if ((executionContext & RenderContext) !== NoContext && root === workInProgressRoot) {
// render phase update 的特殊处理
// ...
} else {
// normal update
// ...
ensureRootIsScheduled(root);
if (
lane === SyncLane &&
executionContext === NoContext &&
!disableLegacyMode &&
(fiber.mode & ConcurrentMode) === NoMode
) {
flushSyncWorkOnLegacyRootsOnly();
}
}
}
你应该在这一节建立两个强约束:
- 更新的“存在”是靠 lanes 标记出来的:root 上的
pendingLanes/suspendedLanes/pingedLanes/...是调度的事实源。 - 调度(分配 CPU 时间)和更新(enqueue)是解耦的:
scheduleUpdateOnFiber的职责是把 root 放到“待调度名单”里,而不是立刻开始 work loop。
为什么要这么做?
- 因为 React 在并发模式下要把多个更新合并、打包、重新排序(lane 的意义之一)。
- 因为浏览器事件结束后才是一个适合“集中决策”的时机(microtask 批处理)。
4) ensureRootIsScheduled:root 维度的“总调度器”与 microtask 策略
很多人读 React 调度只盯着 Scheduler(packages/scheduler),但在今天的 React 里,root scheduler 才是你理解“React 如何启动 work loop”的关键。
ensureRootIsScheduled 做两件事:
- 把 root 挂到一个 root linked list(
firstScheduledRoot/lastScheduledRoot) - 保证有一个 microtask 会在当前浏览器任务结束后跑起来,统一处理 root schedule
源码(packages/react-reconciler/src/ReactFiberRootScheduler.js):
export function ensureRootIsScheduled(root: FiberRoot): void {
// Add the root to the schedule
if (root === lastScheduledRoot || root.next !== null) {
// already scheduled
} else {
if (lastScheduledRoot === null) {
firstScheduledRoot = lastScheduledRoot = root;
} else {
lastScheduledRoot.next = root;
lastScheduledRoot = root;
}
}
mightHavePendingSyncWork = true;
ensureScheduleIsScheduled();
}
这段代码非常“工程化”,但它解决的是一个核心问题:
- 更新可能在一个事件里被触发多次、来自多个 root。
- React 需要一个“批处理窗口”在事件末尾统一做决策(选择 lanes、决定任务优先级、是否同步 flush)。
5) microtask 里发生了什么:scheduleTaskForRootDuringMicrotask 与 getNextLanes
当 microtask 执行时,会遍历所有 scheduled roots,并对每个 root 调用:
scheduleTaskForRootDuringMicrotask(root, currentTime)
这一步决定:
- root 下一次要跑哪些 lanes(
getNextLanes) - 该不该同步 flush(SyncLane)
- 还是要通过 Scheduler(
scheduleCallback(..., performWorkOnRootViaSchedulerTask))跑一个可时间切片的任务
关键代码(ReactFiberRootScheduler.js):
function scheduleTaskForRootDuringMicrotask(
root: FiberRoot,
currentTime: number,
): Lane {
markStarvedLanesAsExpired(root, currentTime);
const rootHasPendingCommit =
root.cancelPendingCommit !== null || root.timeoutHandle !== noTimeout;
const nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
rootHasPendingCommit,
);
if (nextLanes === NoLanes || (root === workInProgressRoot && isWorkLoopSuspendedOnData()) || root.cancelPendingCommit !== null) {
// nothing to work on
// cancel existing callback
return NoLane;
}
if (includesSyncLane(nextLanes) && !checkIfRootIsPrerendering(root, nextLanes)) {
// sync work: flushed at end of microtask
root.callbackPriority = SyncLane;
root.callbackNode = null;
return SyncLane;
}
// We use the highest priority lane to represent the priority of the callback.
const newCallbackPriority = getHighestPriorityLane(nextLanes);
// ... 省略:由 lanesToEventPriority(nextLanes) 推导 schedulerPriorityLevel 的 switch 分支
const newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performWorkOnRootViaSchedulerTask.bind(null, root),
);
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
return newCallbackPriority;
}
5.1 getNextLanes 的直觉:优先级决策不是“一个 lane”,而是一组 lanes
getNextLanes(root, wipLanes, rootHasPendingCommit) 的思路是:
- 先看 root 是否还有
pendingLanes - 如果有非 idle lane,优先从非 idle 中选最高优先级
- 把 suspended/pinged/warm(预热)都考虑进去
- 还要考虑:如果当前正在 render(
wipLanes不为 0),不要轻易中断
源码(ReactFiberLane.js,节选):
export function getNextLanes(
root: FiberRoot,
wipLanes: Lanes,
rootHasPendingCommit: boolean,
): Lanes {
const pendingLanes = root.pendingLanes;
if (pendingLanes === NoLanes) {
return NoLanes;
}
let nextLanes: Lanes = NoLanes;
const suspendedLanes = root.suspendedLanes;
const pingedLanes = root.pingedLanes;
const warmLanes = root.warmLanes;
const nonIdlePendingLanes = pendingLanes & NonIdleLanes;
if (nonIdlePendingLanes !== NoLanes) {
const nonIdleUnblockedLanes = nonIdlePendingLanes & ~suspendedLanes;
if (nonIdleUnblockedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(nonIdleUnblockedLanes);
} else {
const nonIdlePingedLanes = nonIdlePendingLanes & pingedLanes;
if (nonIdlePingedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(nonIdlePingedLanes);
} else {
if (!rootHasPendingCommit) {
const lanesToPrewarm = nonIdlePendingLanes & ~warmLanes;
if (lanesToPrewarm !== NoLanes) {
nextLanes = getHighestPriorityLanes(lanesToPrewarm);
}
}
}
}
}
// ... 处理中断与 Default lane 的特殊规则
return nextLanes;
}
读到这里你会发现:lane 不是“数字优先级”那么简单,而是一个位图语言,它让调度器能做:
- 合并(多个 lane 同时 work)
- 掩码筛选(阻塞/非阻塞、idle/非 idle、transition 子集)
- 中断策略(wip lanes vs next lanes)
6) 真正进入 WorkLoop 的入口:同步 vs 并发两条路径
经过 microtask 决策后,WorkLoop 可能以两种方式进入:
- 同步路径:microtask 末尾 flush(例如某些 SyncLane),走
performSyncWorkOnRoot - 并发路径:Scheduler 任务执行,走
performWorkOnRootViaSchedulerTask→performWorkOnRoot
performSyncWorkOnRoot(ReactFiberRootScheduler.js):
function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes) {
const didFlushPassiveEffects = flushPendingEffects();
if (didFlushPassiveEffects) {
return null;
}
const forceSync = true;
performWorkOnRoot(root, lanes, forceSync);
}
performWorkOnRootViaSchedulerTask(同文件,末尾会走):
const forceSync = !disableSchedulerTimeoutInWorkLoop && didTimeout;
performWorkOnRoot(root, lanes, forceSync);
到这里你应该把“React 何时让出线程”这个问题放到正确的层级上:
- 是否 time slicing:在
performWorkOnRoot里根据 lanes/forceSync 决定。 - 是否有宿主任务:在
RootScheduler里用 microtask + Scheduler 组合决定。
7) render phase:renderRootSync / renderRootConcurrent 的共同骨架
当进入 WorkLoop,render phase 会根据是否允许 time slicing,走两条实现:
renderRootSync(root, lanes, ...)renderRootConcurrent(root, lanes)
它们的共同骨架非常清晰:
- 建立 render 上下文(
executionContext |= RenderContext,push dispatcher) - 准备 Fiber 栈(
prepareFreshStack(root, lanes)) - 循环执行 unit of work(sync:
workLoopSync();并发:workLoopConcurrent(),期间可能 yield) - render 完成后:
finishQueueingConcurrentUpdates()
其中一个对 Hook 更新(第 14 篇)特别重要的点是:
- render 完成后才
finishQueueingConcurrentUpdates() - 这也是为什么并发更新可以先“暂存”在 module-level 队列里,等 render 安全结束再挂到真正的 queue 上
源码(ReactFiberWorkLoop.js,节选):
// Normal case. We completed the whole tree.
workInProgressRoot = null;
workInProgressRootRenderLanes = NoLanes;
// It's safe to process the queue now that the render phase is complete.
finishQueueingConcurrentUpdates();
这里给一个“读源码的锚点”:
- 你只要看到
prepareFreshStack、workLoop*、finishQueueingConcurrentUpdates这三个关键词,就知道你正在看 render phase 的主干。
8) render 结束后不一定立刻 commit:commitRootWhenReady 是“commit 的闸门”
很多人认为 React = render 完成就 commit。事实上,在今天的 React 里,commit 可能被“延后”,原因包括:
- commit 可能需要等待某些 host 资源 ready(Suspensey commit)
- View Transition/gesture 等机制会影响 commit 的时机
这就是 commitRootWhenReady 存在的意义:commit 并不只是 React 内部的事情,它要与 renderer/宿主环境做一次“准备确认”。
源码(ReactFiberWorkLoop.js):
function commitRootWhenReady(
root,
finishedWork,
recoverableErrors,
transitions,
didIncludeRenderPhaseUpdate,
lanes,
spawnedLane,
updatedLanes,
suspendedRetryLanes,
didSkipSuspendedSiblings,
exitStatus,
suspendedCommitReason,
completedRenderStartTime,
completedRenderEndTime,
) {
root.timeoutHandle = noTimeout;
// ... 省略:isViewTransitionEligible/isGestureTransition/timeoutOffset 等推导
const subtreeFlags = finishedWork.subtreeFlags;
const maySuspendCommit =
subtreeFlags & ShouldSuspendCommit ||
(subtreeFlags & (Visibility | MaySuspendCommit)) === (Visibility | MaySuspendCommit);
let suspendedState = null;
if (isViewTransitionEligible || maySuspendCommit || isGestureTransition) {
suspendedState = startSuspendingCommit();
accumulateSuspenseyCommit(finishedWork, lanes, suspendedState);
const schedulePendingCommit = waitForCommitToBeReady(suspendedState, timeoutOffset);
if (schedulePendingCommit !== null) {
pendingEffectsLanes = lanes;
root.cancelPendingCommit = schedulePendingCommit(
commitRoot.bind(null, root, finishedWork, lanes, /* ... */),
);
markRootSuspended(root, lanes, spawnedLane, didAttemptEntireTree);
return;
}
}
// Otherwise, commit immediately.
commitRoot(root, finishedWork, lanes, /* ... */);
}
这一段把你之前读 Suspense/Offscreen/Transition 的“碎片知识”重新拼回主线:
- Suspense 不只是在 render 里 throw promise;它还能影响 commit 的可执行性。
- View Transition 不是一个独立系统,它是 commit 时机的一部分。
如果你要写一句“总结性的心智模型”:
- render phase 决定“长什么样”;commit phase 决定“什么时候落地”。
9) 最后落地:commitRoot 做的是“短且确定”的副作用事务
在第 8 篇我们已经从宏观讲过 commit 阶段的分层(BeforeMutation/Mutation/Layout/Passive)。这里在全链路文章里,我们只抓住它在主线里的位置:
- render 构造出
finishedWork(完成的 Fiber tree) commitRootWhenReady判断能否立即 commitcommitRoot执行副作用,并更新 root 状态(markRootFinished,清 lanes,安排 passive effects)
你能在 commitRoot 的开头看到一个“非常 React 的工程现实”:它会反复 flush pending passive effects,直到没有为止。
源码(ReactFiberWorkLoop.js,节选):
function commitRoot(
root,
finishedWork,
lanes,
recoverableErrors,
transitions,
didIncludeRenderPhaseUpdate,
spawnedLane,
updatedLanes,
suspendedRetryLanes,
exitStatus,
suspendedState,
// ...
): void {
root.cancelPendingCommit = null;
do {
flushPendingEffects();
} while (pendingEffectsStatus !== NO_PENDING_EFFECTS);
// ... markRootFinished / pending passive 等
}
这段逻辑会让你对“为什么 commit 必须短且确定”有更具体的感受:
- 如果 commit 本身也可中断、可 yield,那“副作用的顺序与一致性”会变得非常难保证。
- 所以 React 把可中断性放在 render,把确定性放在 commit。
至于“真正落到 DOM”发生在哪里?
- commit 内部会触发 HostComponent 的 mutation(插入/更新/删除)
- 这些最终会走到
react-dom-bindings的实现(比如创建/更新 DOM 节点、设置属性、处理事件相关副作用)
这部分我们在第 9 篇已经专门展开;这里你只要把它在主线中的位置钉牢即可。
10) 把第 14 篇 Hook 链路接回主线:同一条高速公路的不同匝道
回忆第 14 篇的最短链路:
dispatch(action)→requestUpdateLane(fiber)→ enqueue hook update → 找到 root →scheduleUpdateOnFiber(root, fiber, lane)
现在你可以把它完整延展成“全链路”:
- 匝道(更新产生):Hook queue / class update queue / root container update
- 汇入(标记与入队):
scheduleUpdateOnFiber标记 root lanes +ensureRootIsScheduled - 主路(调度决策):microtask 遍历 roots →
getNextLanes→ 决定同步 flush 或 Scheduler task - 引擎(render/commit):
performWorkOnRoot→renderRoot*→commitRootWhenReady→commitRoot - 落地(renderer side effects):Host mutation/layout/passive effects
给一张“全链路复盘”的 mermaid(你读源码时也可以按这张图逐段对照):
11) 读源码的方法论:用“全链路问题”驱动你读每一个模块
最后一篇,我想留一个“可重复使用”的读源码方法,而不是只把知识点堆完。
当你下一次在源码里迷路时,不要问“这个函数干什么”,而是问“它在全链路里解决哪个矛盾”。举几个例子:
- 为什么要 lane?
- 因为并发渲染需要一种能合并/筛选/中断决策的“优先级语言”。
- 为什么 schedule 不直接 render?
- 因为需要事件末尾批处理,需要 root 维度统一决策。
- 为什么 render/commit 分离?
- 因为可中断性与副作用确定性天然冲突。
- 为什么 commit 还要 whenReady?
- 因为 commit 与宿主环境之间存在“资源 ready / 过渡动画”等协作条件。
当你把这些问题放到正确层级,你就会发现 React 的大量代码不是“复杂”,而是“为了同时满足多约束而不得不工程化”。
总结:把一条更新讲通,你就拥有了读 React 的主干地图
把本文压缩成四句话:
root.render/Hook 更新最终都会汇入scheduleUpdateOnFiber,它负责标记 lanes + 确保 root 被调度。ensureRootIsScheduled把 root 加入调度链表,并用 microtask 在事件末尾统一决策。- microtask 中
getNextLanes决定下一次 work 的 lanes;同步 lane 走同步 flush,并发 lane 走 Scheduler task。 - render 结束后进入
commitRootWhenReady,决定是否能立即 commit;commit 作为“短且确定”的事务把副作用落地到 Host 层。
至此,这个系列的主线就闭环了:
- 入口(ReactDOM)
- 调度语言(Lane)
- 调度执行(RootScheduler + Scheduler)
- render/commit 分层(WorkLoop)
- 平台落地(DOM bindings)
- 扩展边界(Suspense/Offscreen/Hydration/Fizz/RSC)
如果你已经能用本文的图在脑子里“把一次更新跑一遍”,那你再去读任何 React 新特性(比如未来的过渡能力、更多 host 特性),都不会再失去主线。
更多推荐



所有评论(0)