React 渲染机制

React 的一次更新可以分为三个阶段:

  • 触发更新:把更新放进队列,标记优先级,并调度到 Root;
  • Render:构建新的 workInProgress 树,计算这次“要变什么”;
  • Commit:把 Render 阶段算好的变更提交到宿主环境。

Render 负责计算变更,可以被中断、重做,也可以跳过当前优先级下没有工作的子树;Commit 负责提交计算结果,一旦开始就会同步完成本轮提交。

Render 阶段

更新触发后,React 会先记录更新、标记优先级并调度到 Root,再进入 Render 阶段。Render 通过 work loop 以深度优先的方式遍历 Fiber 树。同步与并发渲染使用不同的 work loop,但处理每个 Fiber 时都包含两类工作:

  • “递”:从父到子进入节点,执行 beginWork
  • “归”:从子到父返回节点,执行 completeWork

从触发更新、调度到完成 Render 的整体流程如下:

触发更新

入口通常来自 setStatedispatchroot.render,以及 startTransition 回调中触发的状态更新。这些调用会先把更新放进对应的更新队列,此时尚未开始遍历 Fiber 树。

触发更新主要完成两件事:

  • 记录这次更新的数据;
  • 给这次更新分配 lane,并把调度请求交给 Root。

调度准备

React 会根据更新来源、事件优先级和 Transition 等上下文选择对应的 lane。Root 开始工作时,先从待处理 lanes 中选出本轮的 renderLanes,再创建或复用本轮 Render 的 workInProgress 根节点。

调度准备会确定:

  • 本轮优先处理哪一批更新;
  • 从中断处继续,还是基于当前状态重新开始 Render。

调度准备完成后,beginWork 就有了明确的处理优先级和遍历起点。

BeginWork

当 work loop 取到某个 workInProgress 节点后,会先执行 beginWork。它会处理当前节点的更新,并确定接下来需要处理的子节点。

它通常会做这些事:

  • 处理当前节点待处理的更新;
  • 读取新的 propsstatecontext
  • 执行函数组件或类组件对应的更新逻辑;
  • 产出本轮的 nextChildren
  • 判断当前 Fiber 是否可以 bailout。

所以 beginWork 的重点不是直接操作 DOM,而是先回答两个问题:

  • 当前节点是否需要重新计算;
  • 如果需要,它的下一层 children 应该是什么结构。

Bailout

Bailout 会复用已有结果,跳过当前优先级下不需要重新计算的工作。

它发生在 beginWork 中。React 会先检查当前 Fiber 是否有本轮需要处理的更新,以及 propscontext 等是否发生变化,再根据 Fiber 类型判断能否复用已有结果。

当前 Fiber 能够 bailout,并不意味着整棵子树都会被跳过。如果 childLanes 中包含本轮需要处理的优先级,React 会继续向下遍历;只有子树中没有相关工作时,才会跳过整棵子树。

Reconcile

beginWork 会调用 reconcileChildren,对比 nextChildren 与同一父 Fiber 下的旧子 Fiber。生成的子 Fiber 会挂到 workInProgress.child;首次挂载时没有旧子 Fiber,直接创建。

对于 React Element,diff 分为两种情况:

  • 单节点:先匹配 key,再比较 type
  • 多节点:先按顺序比较,无法继续复用时,再按 key(无 key 时按索引)查找剩余的旧 Fiber。

keytype 匹配时复用旧 Fiber,否则创建新的 Fiber。未复用的旧 Fiber 标记为删除;复用后位置变化的 Fiber 标记为移动。具体的单节点和多节点 diff 过程,见《React Reconcile 实现》

CompleteWork

beginWork 没有返回子节点,或者当前节点的子树已经处理完成时,work loop 会向上回溯,执行 completeWork

completeWork 会根据 Fiber 类型执行对应的收尾逻辑,主要包括:

  • Host Fiber 在 mount 时创建并组装宿主节点,在 update 时标记需要提交的变更;
  • Suspense、Context Provider 等 Fiber 完成各自的状态或上下文处理;
  • 将子节点的 flagssubtreeFlags 汇总到当前 Fiber 的 subtreeFlags,供 Commit 阶段定位需要处理的节点。

当前节点完成后,React 会继续检查它的 sibling

  • sibling,切换到兄弟节点并执行它的 beginWork
  • 没有 sibling,沿 return 返回父节点并执行父节点的 completeWork

Root 完成后,本轮 Render 结束,生成的 workInProgress 树等待进入 Commit 阶段。

Commit 阶段

Render 阶段完成 workInProgress 树的构建后,React 进入 Commit 阶段,根据收集到的 flagssubtreeFlags 提交变更,整体流程如下:

Before Mutation

Before Mutation 发生在 DOM 变更前,主要执行类组件的 getSnapshotBeforeUpdate,用于读取更新前的 DOM 状态。其返回值会在 Layout 阶段传给 componentDidUpdate

Mutation

Mutation 阶段将 Render 收集到的变更提交到宿主环境,主要包括:

  • 插入、更新、删除真实 DOM;
  • 解绑旧 ref;
  • 删除子树时执行类组件的 componentWillUnmount
  • 对于需要更新的 useInsertionEffect,先执行旧的 cleanup,再执行新的 setup;
  • 执行旧的 useLayoutEffect cleanup。

切换 current

Mutation 完成后,React 将 Root 的 current 指针切换到本轮 workInProgress 树,使其成为新的 current Fiber 树。切换发生在 Layout 之前,后续的 componentDidMountcomponentDidUpdateuseLayoutEffect 都会基于新树执行。

Layout

Layout 阶段中,DOM 变更已经完成,但浏览器尚未绘制当前更新。这个阶段会执行需要在绘制前同步完成的工作:

  • 执行类组件的 componentDidMountcomponentDidUpdate,此时可以读取更新后的 DOM;
  • 绑定新的 ref;
  • 执行新的 useLayoutEffect setup。

这一阶段会阻塞浏览器绘制,可用于在绘制前完成布局测量和 DOM 调整。

Passive Effects

Passive Effects 会在 Commit 后安排执行。对于非交互更新,React 通常会先让浏览器绘制,再执行 useEffect;交互更新中的 useEffect 则可能在绘制前执行。依赖发生变化时,React 会先调用上一次 setup 返回的 cleanup,再调用新的 setup。

因此,useEffect 更适合处理订阅、网络连接等外部同步逻辑,不适合用于必须在绘制前完成的视觉调整。

常见问题

1. React 是如何实现并发渲染的?中断后又是如何恢复的?

React 通过时间切片和优先级调度实现并发渲染。Render 过程中,React 会在 workLoopConcurrent 里周期性检查 shouldYield();一旦当前时间片用尽,或者出现了更高优先级任务,就会先中断当前工作,把执行权让出去。

恢复 Render 时,React 会检查当前 Root 和本轮处理的 renderLanes 是否仍与中断前一致:

  • 如果 Root 和 renderLanes 都没变,就沿用原来的 workInProgress,从中断处继续;
  • 如果 Root 或 renderLanes 已经变化,就放弃当前未完成的工作,重新准备 workInProgress 树并从 Root 开始 Render。

2. React 每次触发渲染都是从根节点开始的吗?

Render 的入口是 Root Fiber,但这不代表每次都要重新计算整棵树。更新触发后,React 会记录更新,并将对应的 lane 向上标记到 Root。向下遍历时,React 会通过 bailout 跳过本轮不需要重新计算的节点或子树。

3. Mount 和 Update 是两套不同的机制吗?

Mount 和 Update 共用同一条执行主线:beginWork → reconcileChildren → completeWork → Commit

两者在 Render 阶段的主要区别,是能否复用已有的 current Fiber:

  • Mount 时,新节点没有对应的 current Fiber,需要创建 Fiber 和宿主节点;
  • Update 时,React 会基于 current Fiber 复用结构、处理更新并执行子节点 diff,满足条件时可以 bailout。

进入 Commit 阶段后,Mount 主要插入新的宿主节点,Update 则提交 Render 阶段标记的变更。