- Published on
React 更新渲染流程
在 React 应用的生命周期中,初始挂载仅发生一次,而重渲染 (Re-render) 则是响应状态与属性变更、驱动界面持续更新的核心流程。与初始挂载相比,更新流程更为复杂,它引入了高效的协调算法 (Reconciliation Algorithm) 和优化策略 (Bailout),旨在以最小的性能开销,计算出两次 UI 状态之间的差异,并将其原子性地应用到 DOM 上。
基础架构:Fiber 与 Lanes
更新流程的核心,依然围绕 Fiber 架构展开,但引入了Lanes 模型来管理更新的优先级。
Lanes 是 React 内部用于表示更新优先级的数字位掩码 (bitmask) 系统。不同的用户交互或 API 调用会产生不同优先级的 Lane。
SyncLane: 同步优先级,最高。用于处理用户输入等需要立即反馈的事件。InputContinuousLane: 连续输入事件,如scroll,drag。DefaultLane: 默认优先级,用于普通的状态更新。TransitionLane: 过渡优先级,由startTransition触发,为非紧急更新设计。
每个 Fiber 节点都包含 lanes 和 childLanes 两个属性,用于快速识别该节点及其子树是否存在待处理的、具有特定优先级的更新。
更新渲染的四个阶段
阶段一:触发 (Trigger) — 标记与调度
- 状态更新: 用户交互(如点击按钮)触发
setState()调用,一个更新被创建。 - Lane 标记: React 会为这次更新分配一个 Lane(例如,点击事件对应
SyncLane)。然后,它会从触发更新的组件对应的 Fiber 节点开始,沿着return指针向上遍历至根节点,沿途在每个节点的lanes和childLanes属性上标记上这个新的优先级。 - 任务调度:
scheduleUpdateOnFiber()函数被调用,最终通过ensureRootIsScheduled()将一个渲染任务注册到 React 内部的调度器 (Scheduler) 中。
这个标记过程确保了在后续的渲染阶段,React 可以跳过所有 lanes 和 childLanes 与当前渲染优先级不匹配的子树,是一种高效的剪枝策略。
阶段二:调度 (Schedule) - 任务执行决策
此阶段与初始挂载类似。Scheduler 会根据其优先级队列,决定何时执行渲染任务。由于用户交互产生的 SyncLane 优先级最高,调度器会立即开始执行该渲染任务。
阶段三:渲染 (Render) - 协调 Fiber 树
这是计算变更的核心阶段。对于 SyncLane 的更新,此阶段会以同步、不可中断的方式执行。
workInProgress树与双缓冲: 渲染开始时,React 会调用createWorkInProgress(),它会复用当前 Fiber 树(current树)的备用节点 (alternate) 来作为新的工作进度树 (workInProgress树) 的根。这棵workInProgress树是所有变更计算的“草稿”。
- current:当前显示在屏幕上的 Fiber 树
- workInProgress:正在构建的新 Fiber 树
- alternate:把两棵树的对应节点互相关联的指针
current.alternate === workInProgressworkInProgress.alternate === current
在 commit 阶段:workInProgress 会变成新的 current,而旧的 current 会退居为备用的 alternate。
- 工作循环与
beginWork: 渲染阶段的核心是一个由workLoopSync驱动的循环,它以深度优先遍历的方式处理workInProgress树上的每个 Fiber 节点。- Bailout (跳过) 优化: 在
beginWork中,对于一个 Fiber 节点,如果其pendingProps与memoizedProps相同,且没有待处理的 Context 变化或lanes标记,React 会调用attemptEarlyBailoutIfNoScheduledUpdate()尝试跳过对该组件的重新渲染。 reconcileChildren(Diffing): 如果一个组件无法被跳过,beginWork会调用其函数体,并用新生成的子元素与current树中对应的旧子 Fiber 节点进行比较 (Diffing)。这个过程由reconcileChildFibers函数完成,它会根据key属性来最大化地复用旧的 Fiber 节点,并为需要变更的节点打上相应的副作用标记 (flags),如Placement(插入),Update(更新),Deletion(删除)。
- Bailout (跳过) 优化: 在
completeWork与副作用列表: 当一个 Fiber 节点的所有子节点都处理完毕后,completeWork函数会被调用。它负责为HostComponent(如<div>)准备属性更新的payload,并将其flags冒泡到父节点。最终,在根节点上形成一个完整的副作用列表 (effect list)。
阶段四:提交 (Commit) - 原子化的 DOM 更新
当渲染阶段完成,React 会进入同步的、不可中断的提交阶段,将所有计算出的变更一次性地应用到 DOM 上。
为保证 DOM 操作的效率和一致性,commitMutationEffects 严格遵循“先删、后插、最后更新”的顺序来处理副作用。
- Before Mutation 阶段: 调用
getSnapshotBeforeUpdate生命周期方法,允许组件在 DOM 变更前读取布局信息。 - Mutation 阶段: 遍历副作用列表,执行所有 DOM 节点的实际操作:
- 删除 (Deletion): 调用
removeChild(),并递归地执行组件的卸载逻辑(如componentWillUnmount)。 - 插入 (Placement): 将新创建的 DOM 节点通过
appendChild()或insertBefore()插入到正确的位置。 - 更新 (Update): 对于
HostComponent,应用render阶段准备好的属性变更payload;对于HostText,直接更新其文本内容。
- 删除 (Deletion): 调用
- Layout 阶段: 在 DOM 变更完成后、浏览器绘制前,同步地调用所有
useLayoutEffect的回调和类组件的componentDidMount/componentDidUpdate方法。 - Passive 阶段: 在浏览器绘制完成后,异步地调度
useEffect的回调执行。