渲染后的决策阶段 。它接收 renderRootConcurrent(或 renderRootSync)的 exitStatus,根据本次渲染的退出状态做分支分流:
- 非法状态抛出内部
Bug异常 - 渲染报错:直接进入
commit提交错误树,展示ErrorBoundary - 渲染挂起(
Suspense):判断是否立即提交fallback/ 延时节流等待数据 - 正常渲染完成:直接进入
commit应用DOM变更;全程控制并发渲染是否提交、何时提交、延迟提交 ,是Render → Commit唯一中转枢纽
一、完整决策树
scss
finishConcurrentRender(exitStatus)
│
├─ RootInProgress / RootFatalErrored
│ └─ throw Error (bug)
│
├─ RootSuspendedWithDelay
│ ├─ 含阻塞 lane → break (提交)
│ └─ 仅 transition/retry → fall through to RootSuspendedAtTheShell
│
├─ RootSuspendedAtTheShell
│ └─ markRootSuspended (不提交,等数据)
│
├─ RootErrored
│ ├─ 清除 recoverableErrors
│ └─ break (提交错误状态)
│
├─ RootSuspended
│ └─ break (提交 fallback)
│
└─ RootCompleted
└─ break (提交完成树)
│
▼
act 环境?
├─ Yes → completeRoot 立即提交
└─ No
│
仅 retries + 节流?
├─ Yes → scheduleTimeout(completeRootWhenReady, msUntilTimeout)
│ return (延迟提交)
│
└─ No
│
completeRootWhenReady
├─ 资源就绪 → completeRoot (提交)
├─ 资源未就绪 → schedulePendingCommit(completeRoot) + markRootSuspended
└─ ViewTransition 未完成 → suspendOnActiveViewTransition + waitForCommitToBeReady
二、源码解析
步骤一、exitStatus 分发
情况一、非法状态拦截
javascript
case RootInProgress:
case RootFatalErrored:
throw new Error('Root did not complete. This is a bug in React.');
作用
RootInProgress:表示渲染未完成(时间片用完或中断)进入收尾函数,流程错乱RootFatalErrored:发生不可恢复致命错误,当前workInProgress树直接丢弃,不允许提交- 中断整个
finishConcurrentRender执行,不会走到commit提交DOM
情况二、延迟挂起
javascript
case RootSuspendedWithDelay: {
if (!includesOnlyTransitions(lanes) && !includesOnlyRetries(lanes)) {
break; // ← 包含高优用户交互车道 → 提交
}
}
两种路径:
lanes 组成 |
行为 | 含义 |
|---|---|---|
包含非 transition/retry 的 lane(如 Sync/Default) |
break → 提交 |
阻塞性更新必须立即可见,即使 Suspense 也需渲染 fallback |
仅 transition/retry lane |
fall through → markRootSuspended |
过渡更新可等待,不需要立即显示 fallback,无限等待数据 |
设计意义
RootSuspendedWithDelay 表示我需要等一下,但可以先显示 fallback。然而如果当前更新是过渡 (startTransition)或重试 (Suspense retry),React 认为用户不急于看到结果,所以延迟渲染而不显示 fallback 可以让用户少看到一次闪烁,低优先级后台更新,数据未就绪时维持旧页面
情况三、根层级整体挂起
根级别的 Suspense(如根组件 <App> 被挂起)。此时整个渲染不可提交,因为没有任何 UI 可以显示。React 不提交 placeholder(避免白屏),保持当前 UI,等待数据
javascript
case RootSuspendedAtTheShell: {
markRootSuspended(root, lanes, workInProgressDeferredLane, didAttemptEntireTree);
return; // ← 直接返回,不提交
}
作用
- 标记本次是否完整遍历了整棵树(未跳过任何挂起兄弟节点)
- 核心根状态标记
- 将当前
lanes标记为挂起状态 - 缓存延迟车道、完整遍历标记
- 后续调度更新时,会优先恢复本次挂起渲染
- 将当前
- 直接终止整个函数:不进入
commit,不操作任何DOM,页面维持旧视图
情况四、渲染发生可捕获错误
javascript
case RootErrored: {
workInProgressRootRecoverableErrors = null; // 丢弃可恢复错误
break; // → 提交
}
作用
渲染阶段组件抛出 Error,生成 Throw Fiber,重试渲染后仍无法修复,需要走 ErrorBoundary 降级
- 清空调和阶段收集的临时可恢复错误集合,避免重复、冗余错误传入
commit - 跳出
switch,进入下方公共commit逻辑,提交携带Throw Fiber的WIP树
情况五、普通挂起 + 渲染成功
javascript
case RootSuspended:
case RootCompleted:
break; // → 提交
作用
- 普通高优更新触发
Suspense,可以渲染fallback加载占位 - 整棵树完整渲染无报错、无挂起,存在
DOM增删改副作用 - 仅执行
break跳出switch,无额外状态修改,统一进入下方公共commitRoot提交逻辑
情况六、未知状态兜底
javascript
default: {
throw new Error('Unknown root exit status.');
}
作用
捕获未定义、不存在的 exitStatus 枚举值,抛出明确异常,防止 switch 无匹配静默跳过,导致渲染流程卡死、更新丢失
步骤二、Suspense 重试更新 fallback 节流
仅针对纯数据重试更新 的防抖节流逻辑,解决高频 Suspense 重试导致的加载占位闪烁问题
javascript
if (
// 当前渲染车道全部是 Suspense 数据重试任务,无用户交互、Transition 新更新
includesOnlyRetries(lanes) &&
// 全局开关,强制所有重试更新统一开启节流
// 本次渲染属于普通 Suspense 挂起场景,允许节流防抖
(alwaysThrottleRetries || exitStatus === RootSuspended) // ← 需要节流
) {
// FALLBACK_THROTTLE_MS = 500:节流冷却窗口 500ms
// globalMostRecentFallbackTime:上一次成功渲染 fallback 的时间戳
// 上次展示fallback时间 + 500ms冷却窗口 - 当前时间 = 剩余等待时长
const msUntilTimeout =
globalMostRecentFallbackTime + FALLBACK_THROTTLE_MS - now();
if (msUntilTimeout > 10) {
// 冷却窗口还有充足时间,走延迟提交逻辑
markRootSuspended(
root,
lanes,
workInProgressDeferredLane,
didAttemptEntireTree,
); // ← 标记挂起
// 获取根节点所有未处理、未过期的待更新车道
const nextLanes = getNextLanes(root, NoLanes, true);
if (nextLanes !== NoLanes) {
return; // ← 有其他工作可做
}
// 将定时器句柄挂载在根对象上
root.timeoutHandle = scheduleTimeout(
completeRootWhenReady.bind(
null,
root,
finishedWork,
workInProgressRootRecoverableErrors,
workInProgressTransitions,
workInProgressRootDidIncludeRecursiveRenderUpdate,
lanes,
workInProgressDeferredLane,
workInProgressRootInterleavedUpdatedLanes,
workInProgressSuspendedRetryLanes,
workInProgressRootDidSkipSuspendedSiblings,
exitStatus,
'Throttled',
renderStartTime,
renderEndTime,
),
msUntilTimeout,
);
// 当前渲染流程直接终止,不会走到下方同步 `commitRoot` 逻辑,DOM 不会立即更新。等待定时器到期后,异步执行完整提交渲染 fallback
return; // ← 延迟提交
}
// 间隔极短,无需延迟,执行正常 commit 渲染 fallback
}
作用
- 进入节流分支双重条件
- 本次渲染的所有优先级车道,全部是
Suspense数据重试任务;不存在用户点击、输入、transition这类全新用户发起更新 - 所有重试更新统一开启节流或本次渲染属于普通
Suspense挂起场景,允许节流防抖
- 本次渲染的所有优先级车道,全部是
- 计算距离允许渲染下一个
fallback的剩余冷却毫秒 - 间隔极短,无需延迟,执行正常
commit渲染fallback - 冷却窗口还有充足时间,走延迟提交逻辑
- 标记当前根节点对应车道为挂起状态,持久化本次渲染挂起状态,调度器后续可以识别该更新为挂起任务,不会重复抢占主线程
- 查询是否存在其他待执行更新,有则直接退出函数,放弃创建延迟定时器,把主线程交给其他更高优先级任务
- 注册延迟提交定时器,保存定时器标识到
root - 当前渲染流程直接终止,
DOM不会立即更新。等待定时器到期后,异步执行完整提交渲染fallback
设计意义
防止快速连续的 Suspense fallback 闪烁。默认 500ms 的节流窗口------如果两次 retry 间隔小于 500ms,第二次的 fallback 被延迟。"loading → loaded → loading → loaded" 这种高频切换模式被压缩为 "loading → loaded"(用户看不到中间的第二次 loading)
步骤三、提交前准备
不直接执行 completeRoot 提交 DOM,预处理视图过渡、手势动画、提交可挂起标记,判断是否需要异步等待资源 / 视图过渡就绪再提交
javascript
function completeRootWhenReady(
root,
finishedWork, // 调和完成的 wip 根 Fiber 树
recoverableErrors, // 调和阶段收集的可捕获错误集合
transitions, // 本次渲染关联的 Transition 任务
didIncludeRenderPhaseUpdate, // 渲染阶段是否产生嵌套更新
lanes, // 本次渲染优先级车道
spawnedLane, // 渲染过程派生的延迟车道
updatedLanes, // 本次变更涉及的车道
suspendedRetryLanes, // Suspense 重试车道
didSkipSuspendedSiblings, // 渲染时是否跳过挂起兄弟节点
exitStatus, // 渲染完成退出状态(RootSuspended/RootCompleted/RootErrored)
suspendedCommitReason, // 原始提交挂起原因
// 渲染起止时间戳
completedRenderStartTime,
completedRenderEndTime,
) {
// 清除根上过期定时器句柄
root.timeoutHandle = noTimeout;
// 常量合并两个提交控制标记:可见性控制、提交可挂起
const BothVisibilityAndMaySuspendCommit = Visibility | MaySuspendCommit;
// 整棵子树聚合的提交控制标记
const subtreeFlags = finishedWork.subtreeFlags;
// 全局视图过渡特性开关开启
const isViewTransitionEligible =
enableViewTransition && includesOnlyViewTransitionEligibleLanes(lanes);
// 手势动画特性开关开启
const isGestureTransition = enableGestureTransition && isGestureRender(lanes);
// 判断整棵树是否允许提交阶段挂起
const maySuspendCommit =
subtreeFlags & ShouldSuspendCommit ||
(subtreeFlags & BothVisibilityAndMaySuspendCommit) === BothVisibilityAndMaySuspendCommit;
let suspendedState = null;
if (isViewTransitionEligible || maySuspendCommit || isGestureTransition) {
// 初始化提交挂起上下文、收集整树 Suspense 提交资源
// 创建suspendedState全局提交挂起上下文,存储待等待资源、过渡动画句柄、取消回调
suspendedState = startSuspendingCommit();
// 递归遍历 Fiber 树,收集所有提交阶段需要等待的 Suspense 资源、视图过渡节点,存入suspendedState统一管理
accumulateSuspenseyCommit(finishedWork, lanes, suspendedState);
if (
isViewTransitionEligible ||
(isGestureTransition &&
root.pendingGestures !== null &&
root.pendingGestures.running === null)
) {
// 绑定 DOM 容器视图过渡 API,阻塞提交直到浏览器startViewTransition动画就绪,避免动画撕裂、布局抖动
suspendOnActiveViewTransition(suspendedState, root.containerInfo);
}
// 按更新类型计算等待超时偏移量
const timeoutOffset = includesOnlyRetries(lanes)
? globalMostRecentFallbackTime - now()
: includesOnlyTransitions(lanes)
? globalMostRecentTransitionTime - now()
: 0;
// 启动异步等待调度,获取可取消提交任务
const schedulePendingCommit = waitForCommitToBeReady(suspendedState, timeoutOffset);
if (schedulePendingCommit !== null) {
// 全局标记当前正在等待提交的车道
pendingEffectsLanes = lanes;
// 将取消回调挂载到 FiberRoot,新高优更新触发时可调用清除等待中的提交
root.cancelPendingCommit = schedulePendingCommit(
// 预绑定完整提交函数与全部渲染上下文,资源就绪后执行标准 Commit
completeRoot.bind(
null,
root,
finishedWork,
lanes,
recoverableErrors,
transitions,
didIncludeRenderPhaseUpdate,
spawnedLane,
updatedLanes,
suspendedRetryLanes,
didSkipSuspendedSiblings,
exitStatus,
suspendedState,
enableProfilerTimer ? getSuspendedCommitReason(suspendedState, root.containerInfo) : null,
completedRenderStartTime,
completedRenderEndTime,
),
);
// 标记本次渲染是否完整遍历整棵树,无跳过挂起节点
const didAttemptEntireTree = !didSkipSuspendedSiblings;
// 更新根容器挂起状态,调度器识别该车道处于「等待提交」状态
markRootSuspended(root, lanes, spawnedLane, didAttemptEntireTree);
// 直接终止当前函数,不执行同步 completeRoot,等待异步回调触发提交
return;
}
// 返回null代表无需等待,可同步直接提交
}
completeRoot(
root,
finishedWork,
lanes,
recoverableErrors,
transitions,
didIncludeRenderPhaseUpdate,
spawnedLane,
updatedLanes,
suspendedRetryLanes,
didSkipSuspendedSiblings,
exitStatus,
suspendedState,
suspendedCommitReason,
completedRenderStartTime,
completedRenderEndTime,
);
}
作用
- 清空根节点过期延迟定时器句柄,释放资源,避免内存泄漏,防止后续新渲染误清除本次已执行完毕的定时器
- 读取根
Fiber子树聚合提交标记,定义合并位运算常量 - 判断是否开启视图过渡、手势动画特性,区分动画更新
- 基于子树标记判断提交阶段是否需要等待资源,挂起提交
- 子树包含
ShouldSuspendCommit标记:节点存在提交阶段需加载的资源(图片、字体、媒体) - 子树同时包含
Visibility可见控制、MaySuspendCommit提交挂起双标记
- 子树包含
- 满足动画 / 提交挂起条件时,创建
suspendedState提交挂起上下文,收集所有提交阶段需要等待的Suspense资源、视图过渡节点 - 视图 / 手势动画就绪锁定,对接浏览器原生
ViewTransition API,阻塞提交直到浏览器startViewTransition动画就绪,避免动画撕裂、布局抖动- 纯视图过渡更新
- 手势动画,且当前无正在运行的手势任务(无冲突动画)
- 根据更新类型(重试 /
Transition/ 普通交互)计算等待超时偏移量- 纯
Suspense重试更新:使用fallback加载态时间戳计算偏移 - 纯
Transition低优更新:使用transition全局时间戳 - 普通高优交互更新:偏移量 0,无额外等待
- 纯
- 启动资源等待调度,阻塞式等待所有收集的资源、视图过渡动画就绪,获取可取消的待提交任务
- 若需异步等待
- 注册待提交回调,预绑定完整提交函数与全部渲染上下文,资源就绪后执行标准
Commit - 挂载取消句柄到
root,新高优更新触发时可调用用来清除等待中的提交 - 标记本次渲染是否完整遍历整棵树,无跳过挂起节点
- 标记根挂起,调度器识别该车道处于等待提交状态
- 直接终止当前函数,不执行同步
completeRoot,等待异步回调触发提交
- 注册待提交回调,预绑定完整提交函数与全部渲染上下文,资源就绪后执行标准
- 无需异步等待:同步调用
completeRoot执行标准DOM提交
设计意义
完整并发抢占与可取消设计 :异步等待提交全部提供取消回调,挂载在 FiberRoot;高优先级用户更新到来时,可直接终止低优过渡 / 重试提交,解决并发渲染任务饥饿、过期无效渲染问题
设计思想
- 状态机驱动决策 :整个函数是一个
exitStatus驱动的有限状态机 --------- 6 种状态映射到提交/等待/延迟三种动作 Transition低优延迟 :RootSuspendedWithDelay中transition/retry lane不提交fallback(fall through),阻塞lane必须提交,优先级决定可见性Fallback闪烁抑制 :500msRetry Throttle阻止连续fallback快速切换为用户带来的视觉不适Suspense Commit:completeRootWhenReady中检查资源加载状态、ViewTransition完成状态,DOM提交前确保宿主环境就绪- 可恢复错误策略 :
RootErrored丢弃recoverable errors,主错误足够定位问题,可恢复错误(如水合警告)是噪声