React 渲染机制
React 的一次更新可以分为三个阶段:
- 触发更新:把更新放进队列,标记优先级,并调度到 Root;
- Render:构建新的
workInProgress树,计算这次“要变什么”; - Commit:把 Render 阶段算好的变更提交到宿主环境。
Render 负责计算变更,可以被中断、重做,也可以跳过当前优先级下没有工作的子树;Commit 负责提交计算结果,一旦开始就会同步完成本轮提交。
Render 阶段
更新触发后,React 会先记录更新、标记优先级并调度到 Root,再进入 Render 阶段。Render 通过 work loop 以深度优先的方式遍历 Fiber 树。同步与并发渲染使用不同的 work loop,但处理每个 Fiber 时都包含两类工作:
- “递”:从父到子进入节点,执行
beginWork; - “归”:从子到父返回节点,执行
completeWork。
从触发更新、调度到完成 Render 的整体流程如下:
触发更新
入口通常来自 setState、dispatch、root.render,以及 startTransition 回调中触发的状态更新。这些调用会先把更新放进对应的更新队列,此时尚未开始遍历 Fiber 树。
触发更新主要完成两件事:
- 记录这次更新的数据;
- 给这次更新分配 lane,并把调度请求交给 Root。
调度准备
React 会根据更新来源、事件优先级和 Transition 等上下文选择对应的 lane。Root 开始工作时,先从待处理 lanes 中选出本轮的 renderLanes,再创建或复用本轮 Render 的 workInProgress 根节点。
调度准备会确定:
- 本轮优先处理哪一批更新;
- 从中断处继续,还是基于当前状态重新开始 Render。
调度准备完成后,beginWork 就有了明确的处理优先级和遍历起点。
BeginWork
当 work loop 取到某个 workInProgress 节点后,会先执行 beginWork。它会处理当前节点的更新,并确定接下来需要处理的子节点。
它通常会做这些事:
- 处理当前节点待处理的更新;
- 读取新的
props、state、context; - 执行函数组件或类组件对应的更新逻辑;
- 产出本轮的
nextChildren; - 判断当前 Fiber 是否可以 bailout。
所以 beginWork 的重点不是直接操作 DOM,而是先回答两个问题:
- 当前节点是否需要重新计算;
- 如果需要,它的下一层 children 应该是什么结构。
Bailout
Bailout 会复用已有结果,跳过当前优先级下不需要重新计算的工作。
它发生在 beginWork 中。React 会先检查当前 Fiber 是否有本轮需要处理的更新,以及 props、context 等是否发生变化,再根据 Fiber 类型判断能否复用已有结果。
当前 Fiber 能够 bailout,并不意味着整棵子树都会被跳过。如果 childLanes 中包含本轮需要处理的优先级,React 会继续向下遍历;只有子树中没有相关工作时,才会跳过整棵子树。
Reconcile
beginWork 会调用 reconcileChildren,对比 nextChildren 与同一父 Fiber 下的旧子 Fiber。生成的子 Fiber 会挂到 workInProgress.child;首次挂载时没有旧子 Fiber,直接创建。
对于 React Element,diff 分为两种情况:
- 单节点:先匹配
key,再比较type; - 多节点:先按顺序比较,无法继续复用时,再按
key(无key时按索引)查找剩余的旧 Fiber。
key 和 type 匹配时复用旧 Fiber,否则创建新的 Fiber。未复用的旧 Fiber 标记为删除;复用后位置变化的 Fiber 标记为移动。具体的单节点和多节点 diff 过程,见《React Reconcile 实现》。
CompleteWork
当 beginWork 没有返回子节点,或者当前节点的子树已经处理完成时,work loop 会向上回溯,执行 completeWork。
completeWork 会根据 Fiber 类型执行对应的收尾逻辑,主要包括:
- Host Fiber 在 mount 时创建并组装宿主节点,在 update 时标记需要提交的变更;
- Suspense、Context Provider 等 Fiber 完成各自的状态或上下文处理;
- 将子节点的
flags和subtreeFlags汇总到当前 Fiber 的subtreeFlags,供 Commit 阶段定位需要处理的节点。
当前节点完成后,React 会继续检查它的 sibling:
- 有
sibling,切换到兄弟节点并执行它的beginWork; - 没有
sibling,沿return返回父节点并执行父节点的completeWork。
Root 完成后,本轮 Render 结束,生成的 workInProgress 树等待进入 Commit 阶段。
Commit 阶段
Render 阶段完成 workInProgress 树的构建后,React 进入 Commit 阶段,根据收集到的 flags 和 subtreeFlags 提交变更,整体流程如下:
Before Mutation
Before Mutation 发生在 DOM 变更前,主要执行类组件的 getSnapshotBeforeUpdate,用于读取更新前的 DOM 状态。其返回值会在 Layout 阶段传给 componentDidUpdate。
Mutation
Mutation 阶段将 Render 收集到的变更提交到宿主环境,主要包括:
- 插入、更新、删除真实 DOM;
- 解绑旧 ref;
- 删除子树时执行类组件的
componentWillUnmount; - 对于需要更新的
useInsertionEffect,先执行旧的 cleanup,再执行新的 setup; - 执行旧的
useLayoutEffectcleanup。
切换 current
Mutation 完成后,React 将 Root 的 current 指针切换到本轮 workInProgress 树,使其成为新的 current Fiber 树。切换发生在 Layout 之前,后续的 componentDidMount、componentDidUpdate 和 useLayoutEffect 都会基于新树执行。
Layout
Layout 阶段中,DOM 变更已经完成,但浏览器尚未绘制当前更新。这个阶段会执行需要在绘制前同步完成的工作:
- 执行类组件的
componentDidMount、componentDidUpdate,此时可以读取更新后的 DOM; - 绑定新的 ref;
- 执行新的
useLayoutEffectsetup。
这一阶段会阻塞浏览器绘制,可用于在绘制前完成布局测量和 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 时,新节点没有对应的
currentFiber,需要创建 Fiber 和宿主节点; - Update 时,React 会基于
currentFiber 复用结构、处理更新并执行子节点 diff,满足条件时可以 bailout。
进入 Commit 阶段后,Mount 主要插入新的宿主节点,Update 则提交 Render 阶段标记的变更。