React:如何做到“可中断”的渲染?

457 阅读4分钟

学习React过程中,了解到React有一个称为”可中断渲染”的特点,React通过Fiber架构,实现了渲染的暂停与继续。比较好奇,这部分是如何设计的?

这里的中断有几个关键点点:

  1. 渲染中断让步,不阻塞渲染帧。
  2. 高优先级中断低优先级任务。
  3. 通过双缓存机制保证不影响页面效果。

1. Fiber 架构:渲染的底层单元

核心思想

React 将组件树的渲染过程拆解为多个 Fiber 节点(虚拟的轻量级任务单元),每个 Fiber 节点对应一个组件实例或 DOM 节点。Fiber 的本质是一个链表结构,链表是非连续存储的数据结构,通过指针连接,由于节点可以通过指针连接前后节点,所以可以快速的从某个节点打断与恢复遍历。Fiber节点包含以下关键属性:

{
  tag: FunctionComponent | ClassComponent | HostComponent (DOM节点) | ...,
  type: 'div' | MyComponent, // 组件类型或DOM标签
  stateNode: 实际DOM节点或组件实例,
  return: 父Fiber,
  child: 第一个子Fiber,
  sibling: 兄弟Fiber,
  alternate: 指向另一棵树上的对应Fiber, // 双缓存的关键!
  effectTag: Placement | Update | Deletion, // 标记DOM操作类型
  memoizedProps: 上一次渲染的props,
  memoizedState: Hook链表或Class组件的state,
}

为什么需要 Fiber?

  • 可中断性:传统递归渲染(React 15 及之前)一旦开始就无法中断,可能导致主线程阻塞。
  • 增量渲染:Fiber 的链表结构允许按节点逐步处理,随时暂停或恢复。

2. 双缓存机制(Double Buffering)

什么是双缓存?

React 在内存中同时维护两棵 Fiber 树:

  1. Current Tree:当前屏幕上显示的 UI 对应的 Fiber 树。
  2. WorkInProgress Tree (WIP):正在构建的新 Fiber 树(完成后会替换 Current Tree)。

工作流程

  1. 初始化:首次渲染时,没有 Current Tree,直接构建 WIP 树并提交(变为 Current)。
    Current Tree: null
    WIP Tree: A → B → C (构建完成) → 提交后变为 Current Tree
    
  2. 更新时
    • 从 Current Tree 的根节点克隆出 WIP Tree(通过 alternate 指针关联)。
    • 在 WIP Tree 上执行协调(Reconciliation)和 Diffing。
    • 完成后,WIP Tree 成为新的 Current Tree。
    Current Tree: A → B → C
    WIP Tree: A' → B' → D (更新后)
    Commit Phase: WIP Tree 替换 Current Tree
    

关键点

  • 复用 Fiber 节点:通过 alternate 指针复用之前的节点,避免重复创建对象。
  • 一致性保证:只有完整的 WIP Tree 才会提交,用户不会看到“半渲染”的 UI。
  • 失败回滚:如果渲染中断,直接丢弃 WIP Tree,保持 Current Tree 不变。

图示

Current Tree:    A (div)
                |
                B (p)
                |
                C (span)

WIP Tree:       A' (div)
                |
                B' (p)
                |
                D (button)  ← C被替换为D

提交后,A' → B' → D 成为新的 Current Tree。


3. 时间切片(Time Slicing)

原理

  • 将渲染任务拆分为多个 5ms 左右的小块(避免阻塞主线程)。
  • 通过 React 自研调度器在浏览器空闲时执行任务。
  • 如果超时或更高优先级任务到来,暂停当前渲染,记录断点位置。

4. 调度器(Scheduler)

任务优先级

React 定义了多种优先级(从高到低):

  • Immediate:同步任务(如紧急用户输入)。
  • UserBlocking:点击、动画等。
  • Normal:普通数据更新(默认)。
  • Low:非紧急任务(如预加载)。
  • Idle:空闲时执行(如日志上报)。

中断逻辑

  • 高优先级任务会打断低优先级任务的协调阶段。
  • 中断时,React 保存当前 WIP Tree 的进度(通过 Fiber 链表指针)。

5. 并发模式下的完整流程

阶段一:Render Phase(可中断)

  1. 开始更新setState 或父组件渲染触发。
  2. 协调(Reconciliation)
    • 深度优先遍历 Fiber 树,调用组件渲染函数。
    • 对比新旧 Virtual DOM,标记变更(如 PlacementUpdate)。
    • 可中断点:每个 Fiber 节点处理完后检查时间和优先级。

阶段二:Commit Phase(不可中断)

  1. DOM 突变:将 WIP Tree 的变更一次性提交到真实 DOM。
  2. 生命周期调用:执行 componentDidMountuseLayoutEffect 等。
  3. 切换 Current Tree:WIP Tree 变为 Current Tree。

6. 为什么双缓存能保证一致性?

  • 隔离性:所有变更先在 WIP Tree 上计算,不会影响当前屏幕。
  • 原子性提交:Commit Phase 是同步的,用户看不到中间状态。
  • 错误边界:如果协调阶段出错,直接丢弃 WIP Tree,不会破坏 Current Tree。

总结:React 渲染暂停与继续的完整链条

  1. Fiber 节点 → 2. 双缓存隔离变更 → 3. 时间切片分块执行 → 4. 调度器优先级控制 → 5. 原子提交保证一致性

这种设计使得 React 能实现:

  • 流畅的用户交互:高优先级任务快速响应。
  • 高效的渲染:避免不必要的 DOM 操作。
  • 健壮性:渲染失败不会导致 UI 崩溃。