finishConcurrentRender 源码解析

渲染后的决策阶段 。它接收 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/retrylane(如 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 FiberWIP

情况五、普通挂起 + 渲染成功

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 低优延迟RootSuspendedWithDelaytransition/retry lane 不提交 fallback(fall through),阻塞 lane 必须提交,优先级决定可见性
  • Fallback 闪烁抑制 :500ms Retry Throttle 阻止连续 fallback 快速切换为用户带来的视觉不适
  • Suspense CommitcompleteRootWhenReady 中检查资源加载状态、ViewTransition 完成状态,DOM 提交前确保宿主环境就绪
  • 可恢复错误策略RootErrored 丢弃 recoverable errors,主错误足够定位问题,可恢复错误(如水合警告)是噪声
相关推荐
YFF菲菲兔1 小时前
reconcileChildren 源码解析
react.js
还有多久拿退休金19 小时前
Ant Design Tree 搜索定位避坑指南:虚拟滚动下如何实现高亮与精准定位
前端·react.js
光影少年1 天前
react 原理与进阶
前端·react.js·掘金·金石计划
饼饼饼1 天前
React19 状态解惑:State 没那么神秘,一文读懂 React 状态不可变原则与 Hooks 底层链表
前端·react.js
花椒技术1 天前
RN 多包热更新实践:更新校验、运行时加载与 Bridge 缓存治理
react native·react.js·harmonyos
互联网推荐官1 天前
上海 APP 开发服务甄选:技术架构设计、全维度判断框架
javascript·react native·react.js·app开发·开发经验·上海
饼饼饼2 天前
React19 新手指南:JSX 没那么难,用好这几条规则就够了
前端·javascript·react.js
光影少年2 天前
react大列表优化:虚拟列表原理
前端·javascript·react.js