渲染调和结束后、正式进入 DOM 提交前的统一预处理入口,清理 pending 状态、验证安全性、设置全局效果变量、特殊处理手势过渡(gesture transition)
一、完整调用链
kotlin
completeRoot(root, finishedWork, lanes, recoverableErrors, transitions, didIncludeRenderPhaseUpdate, spawnedLane, updatedLanes, suspendedRetryLanes, didSkipSuspendedSiblings, exitStatus, suspendedState)
│
├─ ① root.cancelPendingCommit = null
├─ ② flushPendingEffects() (do-while)
├─ ③ 安全断言
├─ ④ finishedWork === null? → return
├─ ⑤ 防同一树检查
├─ ⑥ 清除 workInProgressRoot
├─ ⑦ 设 pending 全局变量
├─ ⑧ Gesture 特殊处理? → 延迟提交 + return
└─ ⑨ commitRoot(root, finishedWork, lanes, spawnedLane, updatedLanes, suspendedRetryLanes, suspendedState)
二、源码解析
参数解析
| 字段名 | 含义说明 |
|---|---|
root |
FiberRoot 根容器 |
finishedWork |
调和完成的 workInProgress 根 Fiber,null 代表无 DOM 更新 |
lanes |
本次渲染优先级车道 |
recoverableErrors |
调和捕获的可恢复错误集合(ErrorBoundary 降级用) |
transitions |
本次关联的 useTransition 任务 |
didIncludeRenderPhaseUpdate |
渲染阶段是否产生嵌套 setState 更新 |
spawnedLane |
渲染派生的延迟车道 |
updatedLanes |
本次变更涉及车道 |
suspendedRetryLanes |
Suspense 重试车道 |
didSkipSuspendedSiblings |
渲染是否跳过挂起子节点 |
exitStatus |
渲染结束状态 RootCompleted/RootErrored/RootSuspended |
suspendedState |
提交挂起上下文(视图过渡 / 媒体等待) |
步骤一、清空根容器待取消异步提交句柄
javascript
root.cancelPendingCommit = null;
作用
清除之前 completeRootWhenReady 中设置的可能待执行的 commit 回调(资源加载完成后调用),防止旧回调篡改本次提交
设计意义
当前 completeRoot 正在提交,即资源已完成或无需等待。任何之前注册的待执行 cancelPendingCommit 回调都必须清除,否则会在当前提交后额外触发一次 completeRoot,导致重复提交,统一根状态清理,避免多轮延时提交互相干扰,防止内存泄漏、无效回调执行
步骤二、循环刷新所有 pending 被动副作用
javascript
do {
flushPendingEffects();
} while (pendingEffectsStatus !== NO_PENDING_EFFECTS);
作用
在开始新的 commit 之前,强制刷新任何尚未处理的 passive effects(如 useEffect 回调),执行排队的 useEffect/useLayoutEffect 衍生更新,保证进入提交阶段前渲染阶段所有嵌套更新全部执行完毕
设计意义
嵌套 commit 安全 。flushPendingEffects 内部会调用 flushPassiveEffects,而 flushPassiveEffects 末尾可能调用 flushSyncUpdateQueue,产生新的同步渲染和 commit。do-while 循环确保所有的副作用链都被消费完毕后才开始当前 commit,避免前一个 commit 的被动 effect 在当前 commit 的 DOM 上操作。解决 effect 内部 setState 产生嵌套更新 的闭环问题,本轮 Commit 的 DOM 变更在执行 Effect 之前已经固化,Effect 中触发的状态更新只会进入下一轮渲染流程,在下一次 Commit 才更新真实 DOM
步骤三、禁止嵌套在 Render/Commit 上下文执行提交
javascript
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
throw new Error('Should not already be working.');
}
作用
确保当前不在渲染(RenderContext)或提交(CommitContext)中,completeRoot 不应被递归调用,禁止嵌套在 Render/Commit 上下文执行提交
设计意义
这是 React 的上下文守卫 。executionContext 是一个按位掩码,跟踪当前正在进行的操作。如果已经到了 completeRoot,理论上前面 performWorkOnRoot 中设置的 RenderContext 已被清除。如果仍存在,说明有递归 bug(如提交过程中触发 setState 又导致提交)
步骤四、调和完成树为空保护
javascript
if (finishedWork === null) {
if (enableSchedulingProfiler) { markCommitStopped(); }
if (enableGestureTransition) {
if (isGestureRender(lanes)) { stopCommittedGesture(root); }
}
return;
}
作用
finishedWork 为 null 时跳过整个 commit,不提交任何变更,手势渲染场景终止手势动画
设计意义
这是一个防御性检查。正常情况下 finishedWork 不应为 null,但在某些退化路径(如前述 gesture 特殊处理)中可能调用 completeRoot 返回
步骤五、禁止提交 current 旧树
javascript
if (finishedWork === root.current) {
throw new Error('Cannot commit the same tree as before.');
}
设计意义
root.current 指向当前屏幕上显示的 fiber 树。finishedWork 是刚刚渲染完成的 WiP 树。如果两者是同一个 fiber 对象,说明没有任何变更------重复提交是无意义的,且可能在 mutation 阶段销毁 DOM 节点
步骤六、清空全局 workInProgress 全局缓存(匹配根才重置)
javascript
if (root === workInProgressRoot) {
workInProgressRoot = null;
workInProgress = null;
workInProgressRootRenderLanes = NoLanes;
}
作用
如果当前提交的 root 与 workInProgressRoot 一致,清除所有 WiP 全局变量。延时提交场景(setTimeout / 视图过渡阻塞)会出现渲染根与提交根不一致,保留全局缓存,不强制清空
设计意义
双缓冲交替 :渲染完成后,workInProgressRoot 不再有意义。清除后,下次 ensureRootIsScheduled 会从 root.pendingLanes 重新开始,prepareFreshStack 会创建全新的 WiP 树
步骤七、缓存本次渲染全量上下文至全局 pending 变量
javascript
pendingFinishedWork = finishedWork;
pendingEffectsRoot = root;
pendingEffectsLanes = lanes;
pendingPassiveTransitions = transitions;
pendingRecoverableErrors = recoverableErrors;
pendingDidIncludeRenderPhaseUpdate = didIncludeRenderPhaseUpdate;
作用
将此次 commit 的所有数据存储到全局 pending 变量中,供后续的 flushMutationEffects、flushLayoutEffects、flushPassiveEffects 等阶段函数读取。
设计意义
- 全局状态解耦 。
completeRoot→commitRoot之间通过全局变量传递数据,而不是深层嵌套的函数参数。这样commitRoot中的各个phase函数(flushMutationEffects、flushLayoutEffects)可以在任何时候被调用(如ViewTransition的异步回调中),无需重新传递所有参数 - 解决异步延时提交上下文丢失问题 。隔离渲染临时变量与提交上下文,保证延时回调能拿到完整渲染信息。延时提交(
setTimeout、视图过渡阻塞)过程中,全局workInProgress变量会被新渲染覆盖,因此必须提前把本次提交所需全部上下文存入独立pending全局变量,供后续commitRoot使用
步骤八、手势动画分流逻辑(核心延时分支)
javascript
if (enableGestureTransition && isGestureRender(lanes)) {
const committingGesture = root.pendingGestures;
if (committingGesture !== null && !committingGesture.committing) {
const didAttemptEntireTree = !didSkipSuspendedSiblings;
// 手势未就绪,标记根挂起,注册延时手势提交回调
markRootSuspended(root, lanes, spawnedLane, didAttemptEntireTree);
if (committingGesture.running === null) {
applyGestureOnRoot(
root,
finishedWork,
recoverableErrors,
suspendedState,
enableProfilerTimer
? suspendedCommitReason === null
? completedRenderEndTime
: commitStartTime
: 0,
); // 启动手势动画
} else {
// 已有正在运行手势,放弃本次提交,清空pending上下文用于GC
pendingEffectsRoot = null;
pendingFinishedWork = null;
pendingEffectsLanes = NoLanes;
}
// 注册手势完成后执行完整completeRoot回调
root.cancelPendingCommit = scheduleGestureCommit(
committingGesture,
completeRoot.bind(
null,
root,
finishedWork,
lanes,
recoverableErrors,
transitions,
didIncludeRenderPhaseUpdate,
spawnedLane,
updatedLanes,
suspendedRetryLanes,
didSkipSuspendedSiblings,
exitStatus,
suspendedState,
completedRenderStartTime,
completedRenderEndTime,
)
);
return; // ← 不调用 commitRoot
}
}
作用 :当当前渲染是手势过渡(如手势驱动的动画)时,不立即提交。等待手势动画完成后(如 requestAnimationFrame)再提交
- 识别手势渲染车道,读取根上待执行手势任务
- 手势存在且未进入提交状态
- 标记根车道挂起
- 无运行中手势:绑定新手势动画上下文到根,启动手势动画
- 已有运行手势:清空本次所有
pending提交上下文,回收内存,等待当前手势完成后再提交
- 绑定完整
completeRoot闭包回调,注册到手势完成调度器 return终止函数,不进入commitRoot,等待手势动画结束再提交DOM
设计意义
手势驱动的更新(如拖拽、滑动)不需要立即提交 DOM,先让手势动画流畅运行,手势结束后再同步最终状态。原生手势动画与 React 渲染调度协同,避免动画中途 DOM 变更造成抖动;支持手势期间阻塞提交,动画完成后自动执行更新,提供流畅交互体验
步骤九、无阻塞手势 / 过渡,进入真实 DOM 提交
javascript
commitRoot(
root, finishedWork, lanes, spawnedLane,
updatedLanes, suspendedRetryLanes, suspendedState,
suspendedCommitReason, completedRenderEndTime,
);
作用
所有前置校验、副作用刷新、分流逻辑执行完毕,调用底层 commitRoot,同步执行三阶段提交
三、设计思想
- 全局 pending 变量 :
commit相关数据通过pendingFinishedWork/pendingEffectsRoot等全局变量传递,支持异步回调中提交(如ViewTransition) - 副作用闭环收敛 :
commit前do-while刷新所有pending passive effects,防止嵌套commit的时序问题 - Gesture 退让:手势驱动的更新延迟提交,保证手势动画 60fps 不卡顿
- 防止重复提交 :
cancelPendingCommit清除 +finishedWork === root.current检查,双重保障