React Fiber 架构
React 如何组织组件树、暂停渲染,并把更新提交到页面?
先记住一句话:Fiber 既是 React 的内部数据结构,也是协调工作的基本单元。它让 React 能够保存渲染进度、标记更新优先级,并为可中断 Render 和并发特性提供基础。
Fiber 不等于线程,也不意味着所有 React 更新都会异步执行。它解决的核心问题是:把原本难以暂停的递归协调工作,拆成可以调度的工作单元。
1. Fiber 解决了什么问题?
React 15 主要使用 Stack Reconciler。开始一次更新后,JavaScript 调用栈会持续递归遍历组件树,直到这次工作完成。组件树较大或单个更新计算量较高时,主线程可能长时间无法处理用户输入和绘制。
Fiber 将组件树表示成一组 Fiber 节点,每个节点保存自己的工作状态和与相邻节点的关系。这样 React 可以:
- 在 Render 阶段暂停或继续工作;
- 根据更新优先级安排不同任务;
- 放弃尚未提交的计算结果,重新计算更重要的更新;
- 在 Commit 阶段一次性提交已经完成的结果。
2. Fiber 节点保存什么?
一个 Fiber 可以简化理解成:
其中 return、child 和 sibling 让 React 能用类似链表的方式遍历树,而不必完全依赖 JavaScript 递归调用栈。
memoizedState 在函数组件中还会指向 Hook 链表,因此 useState、useEffect 等 Hook 能和当前组件 Fiber 关联起来。
3. Current 树与 WorkInProgress 树
React 通常维护两棵相关的 Fiber 树:
更新发生时,React 会基于 current 树构建或复用 workInProgress 树,并在其中完成 Render。Render 成功后,React 在 Commit 阶段把 workInProgress 切换为新的 current。
这种方式也常被称为双缓存:新的结果还没完成前,用户继续看到旧的、完整的 UI;未完成的 workInProgress 不会直接污染当前页面。
4. Scheduler 如何控制 Fiber 树执行?
Scheduler 并不是直接“执行整棵 Fiber 树”,而是帮助 React 控制 Render 阶段的工作循环:什么时候开始处理、处理到哪里暂停、下一次从哪里继续,以及哪些更新应该优先完成。
一次更新可以简化成:
这里有四个关键点。
Fiber 是可保存进度的工作单元
Fiber 节点上的 child、sibling 和 return 让 React 可以不用递归调用栈遍历组件树,而是手动决定“下一个要处理的 Fiber”。一个简化的工作循环可以理解为:
因为当前处理到哪个 Fiber 可以被记录下来,所以 Render 阶段才能暂停后继续。
lanes 表示更新优先级
React 会把更新放到不同的优先级中,现代实现里主要通过 lanes 表达。用户输入、点击等需要快速反馈的更新优先级更高;startTransition 标记的列表筛选、页面切换等更新优先级相对更低。
如果低优先级 Render 还没提交,而更高优先级更新到来,React 可以先处理更紧急的任务。尚未提交的 workInProgress 只是计算中的结果,可以被暂停、重做或放弃。
Scheduler 控制让出主线程
在并发渲染中,React 会处理一部分 Fiber,然后判断当前时间片是否快用完,或者是否有更高优先级任务需要处理。如果需要让出主线程,React 会暂停 Render,让浏览器先处理输入、动画或绘制,之后再由 Scheduler 安排继续执行。
需要注意的是,Scheduler 主要控制的是 Render 阶段。Render 只是计算下一棵 workInProgress Fiber 树,还没有真正修改 DOM,因此可以被中断;Commit 阶段会把已经确认的结果提交到宿主环境,通常需要连续完成。
从 root 开始不等于全量更新
React 会从 FiberRoot 开始调度一次更新,但这不代表每个节点都会重新计算。可以这样理解:
React 从 root 开始,是为了保证这次更新在整棵 Fiber 树里保持一致;真正遍历时,会结合 lanes、childLanes、props、state 和 context 判断哪些分支可以跳过。
比如:
Child 更新时,React 会把这次更新的 lane 向父链冒泡:
但 Header 和 Footer 没有相关 lane。下一次从 root Render 时,大致会变成:
所以它更像是“从 root 开始的一次按标记搜索”,不是暴力刷新整棵树。React 里这种跳过通常叫 bailout:如果当前 Fiber 自己没有更新,props、state、context 没变,并且子树的 childLanes 也不包含本次 renderLanes,React 就可以复用之前的结果,不继续深入这棵子树。
还要区分两种情况:如果是 Child 自己 setState,React 可以比较精准地沿父链标记,只进入相关分支;如果是 App 自己 setState,App 函数会重新执行并返回新的 children,下面的子组件是否能跳过,则取决于 React.memo、props 是否稳定、context 是否变化等因素。
5. 一次更新的两个主要阶段
Render 阶段
Render 阶段会:
- 执行函数组件或生命周期相关的计算;
- 根据新的 props 和 state 生成新的 React element,并在协调子节点时执行 Diff;
- 计算节点需要插入、更新、移动或删除的变化;
- 为后续 Commit 阶段设置 flags。
也就是说,Diff 发生在 Render 阶段,而不是 Commit 阶段。Render 阶段会比较新 React element 和旧 Fiber,决定哪些 Fiber 可以复用、哪些需要创建、删除或移动,并把结果记录到 workInProgress Fiber 及其 flags 上。Commit 阶段只是根据这些已经计算好的 flags 执行真实 DOM 操作。
Render 阶段可能被中断、重做或放弃,因此必须保持纯粹。不要在组件函数、useMemo 或其他渲染计算中执行请求、订阅、写 DOM 等副作用。
Commit 阶段
Commit 阶段会把已经确认的结果应用到宿主环境,通常要求快速、连续地完成,以避免页面处于半更新状态。它可以进一步理解为:
- 读取提交前的必要信息;
- 执行 DOM 插入、更新和删除等 mutation;
- 更新 ref,并执行
useLayoutEffect等布局相关工作; - 在提交之后调度
useEffect等 passive effect。
useEffect 并不是在 Render 阶段执行的;Render 可能被放弃,而已执行的副作用无法被 React 自动撤销。
6. 可中断渲染不等于多线程
Fiber 和 Scheduler 让 React 能够将工作拆分并安排优先级,但 React 的 JavaScript 逻辑仍运行在浏览器主线程中。所谓“让出主线程”,是 React 在合适的边界暂停计算,让浏览器处理输入、绘制或其他任务,之后再继续。
因此:
- 一个很重的同步计算函数仍然会阻塞主线程;
startTransition是优先级标记,不会把计算放进 Web Worker;- 可中断主要发生在 Render 阶段,Commit 阶段仍需要提交一致的 UI;
- 并发渲染可能计算多次,只有最终提交的结果才会成为页面状态。
React 18 的 startTransition、useDeferredValue 等公开 API 建立在这些能力之上,详见React 18 并发渲染。
7. Fiber 如何协调子节点?
Fiber 本身不等同于 Diff 算法,但它为协调过程提供了可保存的工作单元。Diff 更准确地说发生在 Render 阶段的协调过程中,尤其是在 beginWork 处理某个 Fiber 的子节点时。
可以把当前 Fiber 的一次协调理解为:
协调时 React 会结合:
- 节点类型;
- 同级位置;
- 列表中的
key; - 当前和下一次 props / state;
判断节点是复用、创建、移动还是删除。列表 key 的具体规则见Virtual DOM 和 Diff 算法。
单节点 Diff
如果新旧节点在同一位置,React 会先看类型和 key:
这里的“创建 Fiber”不等于立刻创建 DOM。Render 阶段只是准备 workInProgress 和 flags,真实 DOM 修改要等到 Commit 阶段。
列表 Diff
列表 Diff 只在同一个父 Fiber 的直接子节点之间比较。React 不会为了寻找最优解在整棵树里跨层级匹配,而是依赖同级位置、类型和 key 做启发式协调。
对于列表:
- key 相同、类型相同:尽量复用旧 Fiber 和组件状态;
- 新 key 找不到旧 Fiber:创建新 Fiber,并在 Commit 阶段插入;
- 旧 key 在新列表中不存在:旧 Fiber 标记删除;
- key 仍存在但位置变化:复用 Fiber,同时标记移动。
因此,key 的核心作用是表达同级节点身份。稳定 key 可以帮助 React 正确复用状态和减少不必要的创建;不稳定 key 或随机 key 会让 React 误以为节点身份变化,导致组件频繁卸载和重建。
8. Fiber 对组件代码的影响
渲染函数必须纯粹
组件可能在 Render 阶段执行多次,因此不要写出依赖“组件函数只执行一次”的代码:
应把外部系统同步放到事件处理函数或 effect 中,并提供清理逻辑:
不要依赖 Render 阶段的中间结果
Render 阶段生成的 workInProgress 可能被放弃。只有 Commit 后的 UI 才是用户真正看到的结果,只有 effect 或事件流程才适合与外部系统建立同步关系。
9. 与旧生命周期的关系
类组件中的以下生命周期可能在旧协调器中表现为一次调用,但在现代 React 的 Render 阶段可能被重复调用,因此被标记为不安全:
componentWillMount/UNSAFE_componentWillMountcomponentWillReceiveProps/UNSAFE_componentWillReceivePropscomponentWillUpdate/UNSAFE_componentWillUpdate
它们不是简单地全部替换成 useEffect:
- 派生状态优先考虑直接计算或
getDerivedStateFromProps; - 与外部系统同步通常使用
useEffect; - 必须在绘制前测量布局时使用
useLayoutEffect; - 组件错误隔离仍可使用 Error Boundary。
10. Fiber、Scheduler、Render、Commit、Effect 的关系
面试总结
可以这样回答:
Fiber 是 React 16 引入的协调架构和数据结构。它把组件树拆成可保存状态的 Fiber 节点,并通过 current / workInProgress 双树支持可中断的 Render。Scheduler 会结合 lanes 优先级安排 Render 工作,在合适时机暂停、继续或切换到更高优先级任务。React 通常从 FiberRoot 开始调度,但会靠 lanes、childLanes 和 bailout 跳过无关分支,不是整棵树都全量重新计算。Diff 发生在 Render 阶段,React 会比较新 element 和旧 Fiber,决定复用、创建、移动或删除,并把结果记录为 flags;Commit 阶段再把这些变化同步提交到 DOM。Fiber 不是线程,React 的副作用不能放在可能重复执行的 Render 阶段。
记住七个关键词:工作单元、双缓存、Scheduler、lanes、bailout、Diff、Render/Commit。

