React Fiber 架构

React 如何组织组件树、暂停渲染,并把更新提交到页面?

先记住一句话:Fiber 既是 React 的内部数据结构,也是协调工作的基本单元。它让 React 能够保存渲染进度、标记更新优先级,并为可中断 Render 和并发特性提供基础。

Fiber 不等于线程,也不意味着所有 React 更新都会异步执行。它解决的核心问题是:把原本难以暂停的递归协调工作,拆成可以调度的工作单元。

1. Fiber 解决了什么问题?

React 15 主要使用 Stack Reconciler。开始一次更新后,JavaScript 调用栈会持续递归遍历组件树,直到这次工作完成。组件树较大或单个更新计算量较高时,主线程可能长时间无法处理用户输入和绘制。

Fiber 将组件树表示成一组 Fiber 节点,每个节点保存自己的工作状态和与相邻节点的关系。这样 React 可以:

  • 在 Render 阶段暂停或继续工作;
  • 根据更新优先级安排不同任务;
  • 放弃尚未提交的计算结果,重新计算更重要的更新;
  • 在 Commit 阶段一次性提交已经完成的结果。

2. Fiber 节点保存什么?

一个 Fiber 可以简化理解成:

Fiber
├── type              // 组件、宿主标签或其他节点类型
├── key               // 列表身份
├── return            // 父 Fiber
├── child             // 第一个子 Fiber
├── sibling           // 下一个兄弟 Fiber
├── memoizedProps     // 已提交的 props
├── memoizedState     // 已提交的 state / Hook 信息
├── flags             // 需要提交的变化标记
├── lanes             // 更新优先级相关信息
└── alternate         // 对应的另一棵 Fiber 树

其中 returnchildsibling 让 React 能用类似链表的方式遍历树,而不必完全依赖 JavaScript 递归调用栈。

memoizedState 在函数组件中还会指向 Hook 链表,因此 useStateuseEffect 等 Hook 能和当前组件 Fiber 关联起来。

3. Current 树与 WorkInProgress 树

React 通常维护两棵相关的 Fiber 树:

Current:已经提交、当前页面对应的树
   ⇅ alternate
WorkInProgress:正在计算下一次更新的树

更新发生时,React 会基于 current 树构建或复用 workInProgress 树,并在其中完成 Render。Render 成功后,React 在 Commit 阶段把 workInProgress 切换为新的 current。

这种方式也常被称为双缓存:新的结果还没完成前,用户继续看到旧的、完整的 UI;未完成的 workInProgress 不会直接污染当前页面。

4. 一次更新的两个主要阶段

状态更新
Render:计算下一棵 Fiber 树
Commit:提交变化到宿主环境

Render 阶段

Render 阶段会:

  • 执行函数组件或生命周期相关的计算;
  • 根据新的 props 和 state 协调子节点;
  • 计算节点需要插入、更新、移动或删除的变化;
  • 为后续 Commit 阶段设置 flags。

Render 阶段可能被中断、重做或放弃,因此必须保持纯粹。不要在组件函数、useMemo 或其他渲染计算中执行请求、订阅、写 DOM 等副作用。

Commit 阶段

Commit 阶段会把已经确认的结果应用到宿主环境,通常要求快速、连续地完成,以避免页面处于半更新状态。它可以进一步理解为:

  1. 读取提交前的必要信息;
  2. 执行 DOM 插入、更新和删除等 mutation;
  3. 更新 ref,并执行 useLayoutEffect 等布局相关工作;
  4. 在提交之后调度 useEffect 等 passive effect。

useEffect 并不是在 Render 阶段执行的;Render 可能被放弃,而已执行的副作用无法被 React 自动撤销。

5. 可中断渲染不等于多线程

Fiber 和 Scheduler 让 React 能够将工作拆分并安排优先级,但 React 的 JavaScript 逻辑仍运行在浏览器主线程中。所谓“让出主线程”,是 React 在合适的边界暂停计算,让浏览器处理输入、绘制或其他任务,之后再继续。

因此:

  • 一个很重的同步计算函数仍然会阻塞主线程;
  • startTransition 是优先级标记,不会把计算放进 Web Worker;
  • 可中断主要发生在 Render 阶段,Commit 阶段仍需要提交一致的 UI;
  • 并发渲染可能计算多次,只有最终提交的结果才会成为页面状态。

React 18 的 startTransitionuseDeferredValue 等公开 API 建立在这些能力之上,详见React 18 并发渲染

6. Fiber 如何协调子节点?

Fiber 本身不等同于 Diff 算法,但它为协调过程提供了可保存的工作单元。协调时 React 会结合:

  • 节点类型;
  • 同级位置;
  • 列表中的 key
  • 当前和下一次 props / state;

判断节点是复用、创建、移动还是删除。列表 key 的具体规则见Virtual DOM 和 Diff 算法

7. Fiber 对组件代码的影响

渲染函数必须纯粹

组件可能在 Render 阶段执行多次,因此不要写出依赖“组件函数只执行一次”的代码:

function Component() {
  // ❌ 不要在渲染期间修改外部变量或发送请求
  analytics.track("render");

  return <div />;
}

应把外部系统同步放到事件处理函数或 effect 中,并提供清理逻辑:

function Component() {
  useEffect(() => {
    const connection = connect();
    return () => connection.disconnect();
  }, []);

  return <div />;
}

不要依赖 Render 阶段的中间结果

Render 阶段生成的 workInProgress 可能被放弃。只有 Commit 后的 UI 才是用户真正看到的结果,只有 effect 或事件流程才适合与外部系统建立同步关系。

8. 与旧生命周期的关系

类组件中的以下生命周期可能在旧协调器中表现为一次调用,但在现代 React 的 Render 阶段可能被重复调用,因此被标记为不安全:

  • componentWillMount / UNSAFE_componentWillMount
  • componentWillReceiveProps / UNSAFE_componentWillReceiveProps
  • componentWillUpdate / UNSAFE_componentWillUpdate

它们不是简单地全部替换成 useEffect

  • 派生状态优先考虑直接计算或 getDerivedStateFromProps
  • 与外部系统同步通常使用 useEffect
  • 必须在绘制前测量布局时使用 useLayoutEffect
  • 组件错误隔离仍可使用 Error Boundary。

9. Fiber、Render、Commit、Effect 的关系

概念主要职责是否可能被中断
Fiber保存节点信息和协调工作状态工作单元可被重新安排
Render计算下一棵树和变化标记可以
Commit把变化提交到 DOM 等宿主环境通常需要连续完成
useLayoutEffectDOM 更新后、绘制前同步处理不属于可中断 Render
useEffect提交后与外部系统同步由 React 在提交后调度

面试总结

可以这样回答:

Fiber 是 React 16 引入的协调架构和数据结构。它把组件树拆成可保存状态的 Fiber 节点,并通过 current / workInProgress 双树支持可中断的 Render。Render 阶段负责执行组件、协调新旧树,可以被暂停或重做;Commit 阶段负责把最终结果同步提交到 DOM。Fiber 不是线程,React 的副作用不能放在可能重复执行的 Render 阶段。

记住四个关键词:工作单元、双缓存、Render/Commit、渲染纯粹性