commitRoot 源码解析

提交阶段真正执行 DOM 变更、生命周期、副作用 的核心入口,不可中断同步 Commit 阶段的总调度器

一、完整调用路径

scss 复制代码
commitRoot 入口
  │
  ├─ markRootFinished(...)        ← 更新 root.pendingLanes
  │
  ├─ 调度 passive effects 回调     ← NormalSchedulerPriority
  │
  ├─ commitBeforeMutationEffects  ← getSnapshotBeforeUpdate
  │    executionContext |= CommitContext
  │
  ├─ 切换 ViewTransition 分支?
  │    ├─ Yes: startViewTransition()  ← 浏览器接管过渡动画
  │    │         ├─ flushMutationEffects
  │    │         ├─ flushAfterMutationEffects
  │    │         ├─ flushLayoutEffects 
  │    │         └─ flushSpawnedWork 
  │    └─ No:
  │         ├─ flushMutationEffects   ← DOM 突变
  │         │    ├─ commitMutationEffects
  │         │    ├─ resetAfterCommit
  │         │    └─ root.current = finishedWork  ← 切换双缓冲
  │         │
  │         ├─ flushLayoutEffects     ← componentDidMount/Update
  │         │    ├─ commitLayoutEffects
  │         │    └─ (useLayoutEffect 同步执行)
  │         │
  │         └─ flushSpawnedWork       ← 后处理
  │              ├─ requestPaint()
  │              ├─ 检查 passive effects
  │              ├─ 调度或释放 cache
  │              └─ onCommitRootDevTools
  └─ (稍后) flushPassiveEffects  ──── useEffect 回调

二、源码解析

步骤一、计算本次渲染占用总车道,合并并发更新车道

javascript 复制代码
// 待保留车道集合
let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);
pendingEffectsRemainingLanes = remainingLanes;
// 获取渲染阶段并发事件新增的更新车道
const concurrentlyUpdatedLanes = getConcurrentlyUpdatedLanes();
remainingLanes = mergeLanes(remainingLanes, concurrentlyUpdatedLanes);

作用

  • 合并根 Fiber 自身车道 + 整棵子树聚合车道,得到本次渲染覆盖的全部车道,提交完后,这些车道可以标记为已完成 ,从 root.pendingLanes 清除
  • 存入全局 pendingEffectsRemainingLanes,供副作用日志、调度器读取
  • 获取渲染阶段并发事件新增的更新车道
  • 合并并发更新车道,不把并发新增更新标记为已完成

设计意义

  • markRootFinished 会从 root.pendingLanes 中减去本次提交的 lanes,但保留上述剩余 lanes ,即本次提交没有覆盖到的更新。交插更新尤其关键:并发渲染过程中用户可触发新 setState,不能直接清空根的 pendingLanes;通过合并并发车道保留未完成更新,避免更新丢失、调度饥饿

步骤二、手势车道清理

javascript 复制代码
if (enableGestureTransition && root.pendingGestures === null) {
  if (enableProfilerTimer && (remainingLanes & GestureLane) !== NoLanes) {
    clearGestureUpdates();
  }
  remainingLanes &= ~GestureLane;
}

作用

当没有待处理的手势时,从剩余 lanes 中移除 GestureLane,防止没有手势的情况下持续调度手势 lane 的渲染。

设计意义

GestureLane 是一种特殊的 lane,在用户触摸/滑动手势期间被设置。手势结束后,pendingGestures 变为 null,此时 GestureLane 不再需要被调度,清除它避免无意义的渲染循环


步骤三、根车道状态收尾(核心调度状态更新)

渲染提交完成后的根状态清理 / 重置核心函数,一轮渲染 + DOM 提交全部完成后,统一更新 FiberRoot 上所有车道相关调度状态

javascript 复制代码
markRootFinished(
  root,
  lanes,             // 本次提交的 lane
  remainingLanes,    // 保留的 lane(未处理 + 交插更新)
  spawnedLane,       // 本次渲染中 spawned 的 lane
  updatedLanes,      // 交插更新 lane
  suspendedRetryLanes, // 挂起重试 lane
);

function markRootFinished(
  root, 
  finishedLanes, 
  remainingLanes, 
  spawnedLane, 
  updatedLanes,
  suspendedRetryLanes,
) {
  // 提交前根上所有排队待执行车道
  const previouslyPendingLanes = root.pendingLanes;
  // 旧排队车道 减去 要保留的车道 = 本轮彻底处理完毕、可以全部清理销毁的车道集合
  const noLongerPendingLanes = previouslyPendingLanes & ~remainingLanes;
  // 重置根核心待调度车道,只保留渲染中途并发新增更新、本轮未处理完的延迟派生车道
  root.pendingLanes = remainingLanes;   
  // 临时标记为挂起等待数据的车道
  root.suspendedLanes = NoLanes;
  // 被高优更新唤醒、需要重新调度的车道
  root.pingedLanes = NoLanes;
  // 预热预渲染车道(预加载 Suspense 资源后台渲染)
  root.warmLanes = NoLanes;
  if (enableDefaultTransitionIndicator) {
    // 过渡指示器车道过滤保留
    root.indicatorLanes &= remainingLanes;
  }
  // 清空车道纠缠标记
  root.entangledLanes = NoLanes;  
  // 重置错误恢复过滤
  root.errorRecoveryDisabledLanes &= remainingLanes;
  // 重置根外壳挂起计数器
  root.shellSuspendCounter = 0;
  // 每条车道绑定的纠缠关联车道
  const entanglements = root.entanglements;
  // 每条车道的过期时间戳
  const expirationTimes = root.expirationTimes;
  // 离屏容器(Offscreen)内缓存的更新队列
  const hiddenUpdates = root.hiddenUpdates;
  // 循环遍历所有已完成车道,批量销毁配套缓存
  let lanes = noLongerPendingLanes;
  while (lanes > 0) {
    // 从位掩码中取出任意一条车道对应的数组下标
    const index = pickArbitraryLaneIndex(lanes);
    const lane = 1 << index;
    // 清空该车道的纠缠绑定、过期时间戳
    entanglements[index] = NoLanes;
    expirationTimes[index] = NoTimestamp;
    // 处理离屏隐藏更新
    const hiddenUpdatesForLane = hiddenUpdates[index];
    if (hiddenUpdatesForLane !== null) {
      // 清空数组引用,释放 GC 内存
      hiddenUpdates[index] = null;
      // 遍历内部所有更新,移除 Offscreen 离屏标记,若组件重新挂载可正常参与渲染
      for (let i = 0; i < hiddenUpdatesForLane.length; i++) {
        const update = hiddenUpdatesForLane[i];
        if (update !== null) {
          update.lane &= ~OffscreenLane;
        }
      }
    }
    // 从待清理集合中移除当前车道,继续循环直到全部清空
    lanes &= ~lane;
  }
  // 处理渲染过程派生的延迟车道 spawnedLane
  if (spawnedLane !== NoLane) {
    // 将派生车道注册到根延迟调度队列,标记为低优延后执行
    markSpawnedDeferredLane(
      root,
      spawnedLane,
      NoLanes,
    );
  }
  // 存在 Suspense 重试车道且本轮渲染无实际 DOM 变更且非旧版同步根节点
  if (
    suspendedRetryLanes !== NoLanes &&
    updatedLanes === NoLanes &&
    !(disableLegacyMode && root.tag === LegacyRoot)
  ) {
    // 筛选出本轮全新生成、之前未存在的重试车道
    const freshlySpawnedRetryLanes =
      suspendedRetryLanes &
      ~(previouslyPendingLanes & ~finishedLanes);
    // 按位或追加到 root.suspendedLanes,标记这些车道处于数据挂起等待状态  
    root.suspendedLanes |= freshlySpawnedRetryLanes;
  }
}

作用 :更新 rootlane 追踪数据结构

  • 将本次已提交的 lanesroot.pendingLanes 中清除,只保留 remainingLanes(含并发新增更新、延迟派生车道),通过位运算拆分已完成待清理车道待保留车道
  • 把未完成的 remainingLanes 留在根待调度队列,仅保留未处理并发更新、延迟车道,下一轮 ensureRootIsScheduled 会自动调度
  • 清空本轮已处理完毕车道对应的辅助缓存:全局挂起、ping 唤醒、预热车道、过渡指示器、错误恢复、顶层挂起计数器
  • 循环清理所有已完成车道的配套缓存:解绑纠缠、清空过期时间、释放离屏更新内存、清除 Offscreen 标记
  • 处理渲染派生延迟车道 spawnedLane(渲染调和过程中新生成的延迟任务车道-Suspense 恢复、Transition 衍生更新)、将派生车道注册到根延迟调度队列,标记为低优延后执行
  • 处理本轮新产生的 Suspense 重试挂起车道 suspendedRetryLanes,筛选出本轮全新生成、之前未存在的重试车道,按位或追加到 root.suspendedLanes,标记这些车道处于数据挂起等待状态。仅纯 fallback 加载渲染才持久化 Suspense 重试挂起车道,真实数据渲染完成无需保留重试标记

设计意义

  • 调度器依靠该函数区分 "已完成更新" 和 "待执行更新",是并发调度饥饿防护、更新排队的底层支撑
  • 基于位掩码的车道集合管理:全部车道集合操作使用按位与 / 或 / 取反,集合拆分、过滤、合并均为 O (1) 运算;仅在清理缓存时按需循环已完成车道,无无效遍历,适配高频并发更新场景
  • 分层分离两类车道生命周期
    • 已完成车道(noLongerPendingLanes):配套缓存全部销毁、内存释放、标记清零
    • 待保留车道(remainingLanes):完整保留在 pendingLanes,包含渲染中途并发新增用户更新,彻底解决并发更新丢失问题

步骤四、重置提交阶段更新标记

javascript 复制代码
didIncludeCommitPhaseUpdate = false;

作用

标记提交阶段是否产生嵌套 setState,重置为初始 false;在 mutation/layout 阶段(如 componentDidMount 中调用 setState)若触发更新会置为 true,用于后续重新调度渲染

设计意义

检测提交阶段副作用内嵌套更新,保证提交完成后自动发起新一轮渲染,不丢失同步副作用更新


步骤五、视图过渡:区分被动副作用掩码

javascript 复制代码
let passiveSubtreeMask;
if (enableViewTransition) {
  if (includesOnlyViewTransitionEligibleLanes(lanes)) {
    // 清空过渡事件缓存
    pendingTransitionTypes = claimQueuedTransitionTypes(root);
    // 包含 Transition 相关 flags
    passiveSubtreeMask = PassiveTransitionMask;    
  } else {
    // 标准 passive flags
    passiveSubtreeMask = PassiveMask;               
  }
} else {
  passiveSubtreeMask = PassiveMask;
}

作用

  • 开启视图过渡时清空过渡事件缓存
  • 纯视图过渡更新:抢占排队过渡类型,使用 PassiveTransitionMask 掩码(专属过渡副作用)
  • 普通更新使用基础 PassiveMask(常规 useEffect

设计意义

ViewTransition 过渡动画有专属被动副作用逻辑,通过掩码区分普通 effect 和过渡 effect,互不干扰,适配浏览器原生页面快照动画


步骤六、判断是否存在被动副作用,调度异步 useEffect 任务

javascript 复制代码
// 子树含被动标记或根自身含被动标记
if (
  (finishedWork.subtreeFlags & passiveSubtreeMask) !== NoFlags ||
  (finishedWork.flags & passiveSubtreeMask) !== NoFlags
) {
  if (enableYieldingBeforePassive) {
  } else {
    // 有 passive effects → 调度回调
    root.callbackNode = null;
    root.callbackPriority = NoLane;
    scheduleCallback(NormalSchedulerPriority, () => {
      // 标记本次提交属于延迟被动副作用提交
      if (pendingDelayedCommitReason === IMMEDIATE_COMMIT) {
        pendingDelayedCommitReason = DELAYED_PASSIVE_COMMIT;
      }
      flushPassiveEffects();
      return null;
    });
  }
  
} else {
  // 无 passive effects → 清理调度
  root.callbackNode = null;
  root.callbackPriority = NoLane;
}

作用

  • 旧版调度:先检查 fiber 树中是否有 Passive flag(即 useEffect 回调),如果有,立即向 Scheduler 注册普通优先级异步回调NormalSchedulerPriority ,延迟执行 flushPassiveEffects(所有 useEffect 统一在此执行)。被动副作用与页面渲染更新是两条完全独立的 Scheduler 任务队列,重复渲染会产生大量零散独立回调,增加调度器任务队列压力
  • 新版让步调度 enableYieldingBeforePassive:移除独立回调,复用根调度逻辑,渲染主任务(render + commit)和 passive 副作用合并到同一个 Scheduler 任务
  • 无副作用时清空根上缓存的回调句柄,释放内存

设计意义

  • 任务合并优化 :把剩余渲染更新被动副作用批量执行 合并到同一个调度回调,减少 Scheduler 队列任务碎片
  • 状态统一管控 :全部调度逻辑收敛到 ensureRootIsScheduled,根的 callbackNodecallbackPriority 统一管理,避免多处重复创建 / 清除回调

步骤七、重置视图过渡启动标记

javascript 复制代码
resetShouldStartViewTransition();

作用

清空全局视图过渡启动标记,避免上一轮过渡状态污染本次提交;后续 DOM 操作完成后重新判定是否开启新过渡动画。

设计意义

视图过渡状态全局单例,每次提交前置重置,保证每轮更新独立判断动画条件


步骤八、Before Mutation 阶段(快照阶段)

javascript 复制代码
const subtreeHasBeforeMutationEffects =
  (finishedWork.subtreeFlags & (BeforeMutationMask | MutationMask)) !== NoFlags;
const rootHasBeforeMutationEffect =
  (finishedWork.flags & (BeforeMutationMask | MutationMask)) !== NoFlags;

if (subtreeHasBeforeMutationEffects || rootHasBeforeMutationEffect) {
  const prevTransition = ReactSharedInternals.T;
  ReactSharedInternals.T = null;                     // 清除 Transition 上下文
  const previousPriority = getCurrentUpdatePriority();
  setCurrentUpdatePriority(DiscreteEventPriority);   // 设置为离散事件优先级
  const prevExecutionContext = executionContext;
  executionContext |= CommitContext;                  // 设置 CommitContext
  try {
    commitBeforeMutationEffects(root, finishedWork, lanes);
  } finally {
    executionContext = prevExecutionContext;
    setCurrentUpdatePriority(previousPriority);
    ReactSharedInternals.T = prevTransition;
  }
}

作用

  • 通过根 Fiber 预聚合 subtreeFlags 位标记,O (1) 判断整棵树是否存在快照、DOM 修改副作用,无需递归遍历全树
  • 清除 Transition 上下文,getSnapshotBeforeUpdate 不应感知 Transition
  • 切换为离散事件最高优先级、标记进入 Commit 上下文
  • 执行 commitBeforeMutationEffects:遍历 effect 链表执行 getSnapshotBeforeUpdate,读取变更前 DOM 布局、尺寸、滚动位置;
  • 完整恢复全部全局现场,防止上下文污染后续逻辑

设计意义

快照必须在 DOM 修改前同步读取,避免布局抖动;使用 try/finally 保证全局上下文、优先级、Transition 状态一定会恢复,防止状态泄漏


步骤九、手势提交收尾

javascript 复制代码
if (enableGestureTransition) {
  if (isGestureRender(lanes)) {
    stopCommittedGesture(root);
  }
}

作用

本次渲染属于手势交互更新,DOM 提交完成后终止手势动画状态,清空根 pendingGestures 标记。

设计意义

手势生命周期与提交阶段绑定,DOM 变更落地后结束交互动画,避免残留手势阻塞后续更新


步骤十、进入 DOM变更阶段

javascript 复制代码
// 标记进入 DOM 变更阶段
pendingEffectsStatus = PENDING_MUTATION_PHASE;

if (enableViewTransition && shouldStartViewTransition) {
  // ── ViewTransition 异步路径 ──
  if (enableProfilerTimer && enableComponentPerformanceTrack) {
    startAnimating(lanes);
  }
  pendingViewTransition = startViewTransition(
    suspendedState,
    root.containerInfo,
    pendingTransitionTypes,
    flushMutationEffects,         // ← stage 1: DOM 突变
    flushLayoutEffects,           // ← stage 2: layout effects
    flushAfterMutationEffects,    // ← stage 3: after mutation effects
    flushSpawnedWork,             // ← stage 4: spawned work
    flushPassiveEffects,          // ← stage 5: passive effects
    reportViewTransitionError,
    enableProfilerTimer ? suspendedViewTransition : null,
    enableProfilerTimer ? finishedViewTransition.bind(null, lanes) : null,
  );
} else {
  // ── 同步路径 ──
  flushMutationEffects();        // ① DOM 突变
  flushLayoutEffects();          // ② Layout effects
  flushSpawnedWork();            // ③ 后处理
}

作用

  • 全局标记当前副作用阶段为 DOM 变更阶段
  • 开启视图过渡
    • 启动浏览器原生 document.startViewTransition
    • 将三段同步副作用(mutation/layout/afterMutation)、派生更新、被动 effect 全部作为回调传给过渡 API
    • 浏览器会先捕获页面快照,再执行 DOM 修改,最后播放过渡动画,解决页面切换撕裂问题
  • 普通更新
    • flushMutationEffects:第二阶段,真实 DOM 增删改、卸载组件、更新属性、删除旧 DOM
    • flushLayoutEffects:第三阶段,同步布局副作用 useLayoutEffectcomponentDidMount/componentDidUpdateref 赋值
    • flushSpawnedWork:执行渲染 / 提交过程中派生的延迟更新、Suspense 恢复任务

设计意义

  • 对接原生 Web API,把同步 DOM 变更逻辑封装在浏览器动画生命周期内,兼顾 React 副作用体系与原生过渡动画能力
  • 普通更新同步串行执行三段提交,严格遵守执行顺序:快照 → DOM 修改 → 同步布局副作用,保证 DOM 操作与生命周期执行顺序稳定可预测

三、设计思想

  • 三阶段严格分离Before Mutation(读 DOM)→ Mutation(写 DOM)→ Layout(读 DOM + 同步 effect
  • 独立分流视图过渡、手势交互两套特殊提交流程 :ViewTransition 异步提交,通过 startViewTransitionDOM 突变和 layout 推迟到浏览器过渡动画帧中执行,不影响动画流畅度,
  • 并发调度闭环设计 :前置通过 mergeLanes + markRootFinished 统一管理根待更新车道,区分渲染阶段新增并发更新,不会误清空排队任务;提交完成后自动暴露剩余车道给上层 ensureRootIsScheduled,形成更新调度→渲染调和→提交收尾→重新调度完整闭环,解决并发任务饥饿问题
  • DiscreteEventPriority 设置commit 三阶段中优先级临时提升为 DiscreteEventPriority,确保同步不可中断
  • CommitContext 保护 :三阶段中设置 executionContext |= CommitContext,阻止 commit 期间的 setState 触发新渲染
相关推荐
光影少年1 天前
react批量更新、同步/异步更新场景
前端·react.js·掘金·金石计划
YFF菲菲兔1 天前
completeRoot 源码解析
react.js
光影少年2 天前
React 合成事件机制、和原生事件区别、事件冒泡阻止
前端·react.js·掘金·金石计划
YFF菲菲兔2 天前
finishConcurrentRender 源码解析
react.js
YFF菲菲兔2 天前
reconcileChildren 源码解析
react.js
还有多久拿退休金3 天前
Ant Design Tree 搜索定位避坑指南:虚拟滚动下如何实现高亮与精准定位
前端·react.js
光影少年3 天前
react 原理与进阶
前端·react.js·掘金·金石计划
饼饼饼3 天前
React19 状态解惑:State 没那么神秘,一文读懂 React 状态不可变原则与 Hooks 底层链表
前端·react.js
花椒技术3 天前
RN 多包热更新实践:更新校验、运行时加载与 Bridge 缓存治理
react native·react.js·harmonyos