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.js
  • packages/react-reconciler/src/ReactFiberReconciler.js
  • packages/react-reconciler/src/ReactFiberWorkLoop.js
  • packages/react-reconciler/src/ReactFiberRootScheduler.js
  • packages/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

updateContainerReactFiberReconciler.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 lanesmarkRootUpdated(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();
    }
  }
}

你应该在这一节建立两个强约束:

  1. 更新的“存在”是靠 lanes 标记出来的:root 上的 pendingLanes/suspendedLanes/pingedLanes/... 是调度的事实源。
  2. 调度(分配 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 里发生了什么:scheduleTaskForRootDuringMicrotaskgetNextLanes

当 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 任务执行,走 performWorkOnRootViaSchedulerTaskperformWorkOnRoot

performSyncWorkOnRootReactFiberRootScheduler.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)

它们的共同骨架非常清晰:

  1. 建立 render 上下文executionContext |= RenderContext,push dispatcher)
  2. 准备 Fiber 栈prepareFreshStack(root, lanes)
  3. 循环执行 unit of work(sync:workLoopSync();并发:workLoopConcurrent(),期间可能 yield)
  4. 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();

这里给一个“读源码的锚点”:

  • 你只要看到 prepareFreshStackworkLoop*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 判断能否立即 commit
  • commitRoot 执行副作用,并更新 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)performWorkOnRootrenderRoot*commitRootWhenReadycommitRoot
  • 落地(renderer side effects):Host mutation/layout/passive effects

给一张“全链路复盘”的 mermaid(你读源码时也可以按这张图逐段对照):

yes

no

入口:root.render / setState / dispatch

requestUpdateLane

enqueueUpdate / enqueueConcurrentHookUpdate

scheduleUpdateOnFiber

markRootUpdated

ensureRootIsScheduled

microtask: scheduleTaskForRootDuringMicrotask

getNextLanes

includesSyncLane?

performSyncWorkOnRoot -> performWorkOnRoot

scheduleCallback -> performWorkOnRootViaSchedulerTask

renderRootSync / renderRootConcurrent

finishQueueingConcurrentUpdates

commitRootWhenReady

commitRoot

Host effects -> DOM bindings 落地


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 特性),都不会再失去主线。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐