Published on

React 更新渲染流程

在 React 应用的生命周期中,初始挂载仅发生一次,而重渲染 (Re-render) 则是响应状态与属性变更、驱动界面持续更新的核心流程。与初始挂载相比,更新流程更为复杂,它引入了高效的协调算法 (Reconciliation Algorithm)优化策略 (Bailout),旨在以最小的性能开销,计算出两次 UI 状态之间的差异,并将其原子性地应用到 DOM 上。

基础架构:Fiber 与 Lanes

更新流程的核心,依然围绕 Fiber 架构展开,但引入了Lanes 模型来管理更新的优先级。

Lane 优先级模型

Lanes 是 React 内部用于表示更新优先级的数字位掩码 (bitmask) 系统。不同的用户交互或 API 调用会产生不同优先级的 Lane。

  • SyncLane: 同步优先级,最高。用于处理用户输入等需要立即反馈的事件。
  • InputContinuousLane: 连续输入事件,如 scroll, drag
  • DefaultLane: 默认优先级,用于普通的状态更新。
  • TransitionLane: 过渡优先级,由 startTransition 触发,为非紧急更新设计。

每个 Fiber 节点都包含 laneschildLanes 两个属性,用于快速识别该节点及其子树是否存在待处理的、具有特定优先级的更新。

更新渲染的四个阶段

阶段一:触发 (Trigger) — 标记与调度

  1. 状态更新: 用户交互(如点击按钮)触发 setState() 调用,一个更新被创建。
  2. Lane 标记: React 会为这次更新分配一个 Lane(例如,点击事件对应 SyncLane)。然后,它会从触发更新的组件对应的 Fiber 节点开始,沿着 return 指针向上遍历至根节点,沿途在每个节点的 laneschildLanes 属性上标记上这个新的优先级。
  3. 任务调度: scheduleUpdateOnFiber() 函数被调用,最终通过 ensureRootIsScheduled() 将一个渲染任务注册到 React 内部的调度器 (Scheduler) 中。

这个标记过程确保了在后续的渲染阶段,React 可以跳过所有 laneschildLanes 与当前渲染优先级不匹配的子树,是一种高效的剪枝策略。

阶段二:调度 (Schedule) - 任务执行决策

此阶段与初始挂载类似。Scheduler 会根据其优先级队列,决定何时执行渲染任务。由于用户交互产生的 SyncLane 优先级最高,调度器会立即开始执行该渲染任务。

阶段三:渲染 (Render) - 协调 Fiber 树

这是计算变更的核心阶段。对于 SyncLane 的更新,此阶段会以同步、不可中断的方式执行。

  • workInProgress 树与双缓冲: 渲染开始时,React 会调用 createWorkInProgress(),它会复用当前 Fiber 树(current 树)的备用节点 (alternate) 来作为新的工作进度树 (workInProgress 树) 的根。这棵 workInProgress 树是所有变更计算的“草稿”。
current / workInProgress / alternate 的关系

  • current:当前显示在屏幕上的 Fiber 树
  • workInProgress:正在构建的新 Fiber 树
  • alternate:把两棵树的对应节点互相关联的指针
  • current.alternate === workInProgress
  • workInProgress.alternate === current

在 commit 阶段:workInProgress 会变成新的 current,而旧的 current 会退居为备用的 alternate

  • 工作循环与 beginWork: 渲染阶段的核心是一个由 workLoopSync 驱动的循环,它以深度优先遍历的方式处理 workInProgress 树上的每个 Fiber 节点。
    • Bailout (跳过) 优化: 在 beginWork 中,对于一个 Fiber 节点,如果其 pendingPropsmemoizedProps 相同,且没有待处理的 Context 变化或 lanes 标记,React 会调用 attemptEarlyBailoutIfNoScheduledUpdate() 尝试跳过对该组件的重新渲染。
    • reconcileChildren (Diffing): 如果一个组件无法被跳过,beginWork 会调用其函数体,并用新生成的子元素与 current 树中对应的旧子 Fiber 节点进行比较 (Diffing)。这个过程由 reconcileChildFibers 函数完成,它会根据 key 属性来最大化地复用旧的 Fiber 节点,并为需要变更的节点打上相应的副作用标记 (flags),如 Placement (插入), Update (更新), Deletion (删除)。
  • completeWork 与副作用列表: 当一个 Fiber 节点的所有子节点都处理完毕后,completeWork 函数会被调用。它负责为 HostComponent(如 <div>)准备属性更新的 payload,并将其 flags 冒泡到父节点。最终,在根节点上形成一个完整的副作用列表 (effect list)

阶段四:提交 (Commit) - 原子化的 DOM 更新

当渲染阶段完成,React 会进入同步的、不可中断的提交阶段,将所有计算出的变更一次性地应用到 DOM 上。

DOM 操作的精确顺序

为保证 DOM 操作的效率和一致性,commitMutationEffects 严格遵循“先删、后插、最后更新”的顺序来处理副作用。

  1. Before Mutation 阶段: 调用 getSnapshotBeforeUpdate 生命周期方法,允许组件在 DOM 变更前读取布局信息。
  2. Mutation 阶段: 遍历副作用列表,执行所有 DOM 节点的实际操作:
    • 删除 (Deletion): 调用 removeChild(),并递归地执行组件的卸载逻辑(如 componentWillUnmount)。
    • 插入 (Placement): 将新创建的 DOM 节点通过 appendChild()insertBefore() 插入到正确的位置。
    • 更新 (Update): 对于 HostComponent,应用 render 阶段准备好的属性变更 payload;对于 HostText,直接更新其文本内容。
  3. Layout 阶段: 在 DOM 变更完成后、浏览器绘制前,同步地调用所有 useLayoutEffect 的回调和类组件的 componentDidMount/componentDidUpdate 方法。
  4. Passive 阶段: 在浏览器绘制完成后,异步地调度 useEffect 的回调执行。