03|从 `ensureRootIsScheduled` 到 `commitRoot`:React 工作循环(WorkLoop)全景

03|从 ensureRootIsScheduledcommitRoot:React 工作循环(WorkLoop)全景

本栏目是「React 源码剖析」系列:我会以源码为证据、以架构为线索,讲清 React 从构建、运行时到生态边界的关键设计。开源仓库:https://github.com/facebook/react

引言:这一篇在整个库里扮演什么角色?

前两篇我们把"入口层"打通了:

  • createRoot 创建 Root
  • root.render 触发 updateContainer
  • updateContainerImpl 最终调用 scheduleUpdateOnFiber

这一篇我们正式进入 React 的"发动机舱":

  • Root Scheduler(根调度器):负责把"root 有活干"变成"排一个任务(task/microtask)去干"
  • WorkLoop(工作循环):负责在 render/commit 两大阶段里把工作一步步推进,并在需要时 yield

你会看到 React 的核心设计主张:

  • 把渲染变成可调度的工作(work),而不是一次函数调用
  • 把副作用(DOM/生命周期/effects)集中在 commit,并拆成多个子阶段
  • 把所有"何时做"统一交给调度层(Lane + Scheduler)

本文引用的主要源码文件:

  • packages/react-reconciler/src/ReactFiberWorkLoop.js
  • packages/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:当前 root
  • workInProgress:当前 fiber

6) pendingEffectsStatus

Commit 不是"一口气做完",而是拆成多个状态:

  • PENDING_MUTATION_PHASE
  • PENDING_LAYOUT_PHASE
  • PENDING_AFTER_MUTATION_PHASE
  • PENDING_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 更新了。
  • 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)。
  • renderRootConcurrent vs renderRootSync
    • 两条路径最终都构建出 finishedWorkroot.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 个设计思想

  1. scheduleUpdateOnFiber 是登记处,不是施工队

它只负责把 lane 记到 root 上,并触发根调度器。

  1. React 自己维护"root schedule",再把执行交给 Scheduler

这层让 React 能更自由地实现 microtask batching、multi-root、以及对特殊阶段(passive/commit)的处理。

  1. microtask 是现代 React 的关键节拍器
  • 事件内聚合更新
  • microtask 里统一决策
  • microtask 尾部 flush sync work
  1. WorkLoop 的本质是可恢复的 DFS

workInProgress 指针 + beginWork/completeWork 两阶段,保证随时可中断、可恢复。

  1. Commit 是一个分阶段的状态机,不是一坨"副作用"

BeforeMutation / Mutation / Layout / Passive 拆开,分别满足不同的语义要求与性能目标。


下一篇预告

第 4 篇我们会把这一章里多次出现但尚未深入的主角拉出来单讲:

  • Lane 位图优先级如何影响 getNextLanes
  • entangledRenderLanes/Transitions 为什么需要"纠缠(entangle)"
  • includesExpiredLanemarkStarvedLanesAsExpired 如何避免饿死
相关推荐
时光少年1 天前
ExoPlayer MediaCodec视频解码Buffer模式GPU渲染加速
前端
hxjhnct1 天前
Vue 自定义滑块组件
前端·javascript·vue.js
华仔啊1 天前
JavaScript 中如何正确判断 null 和 undefined?
前端·javascript
weibkreuz1 天前
函数柯里化@11
前端·javascript·react.js
king王一帅1 天前
Incremark 0.3.0 发布:双引擎架构 + 完整插件生态,AI 流式渲染的终极方案
前端·人工智能·开源
Cool_so_cool1 天前
vue3实现地图考勤打卡Leaflet 地图库结合 Leaflet Draw 插件
前端框架
转转技术团队1 天前
HLS 流媒体技术:畅享高清视频,忘却 MP4 卡顿的烦恼!
前端
程序员的程1 天前
我做了一个前端股票行情 SDK:stock-sdk(浏览器和 Node 都能跑)
前端·npm·github
KlayPeter1 天前
前端数据存储全解析:localStorage、sessionStorage 与 Cookie
开发语言·前端·javascript·vue.js·缓存·前端框架