跳到主要内容

第 3 章:Fiber——React 的数据结构基石

第 2 章说元素是一张"图纸"。但图纸不能跑起来,React 得把图纸变成能增删改查的东西。这个东西就是 Fiber

先看一个极简组件树:

function App() {
return (
<div>
<span>你好</span>
<button>点我</button>
</div>
);
}

它对应的 Fiber 树,长这样:

先记住这一句:元素是一次性的图纸,渲染一次就扔;Fiber 是持久化的施工结构,可复用、可更新、可中断。元素和 Fiber 一一对应,图纸变成结构的过程叫协调(第 7 章讲算法,本章讲数据结构)。

为什么需要 Fiber

React 16 之前,渲染是递归的:render 一棵树,一口气算到底。树一大就卡死主线程,用户点个按钮,画面半天不动。要支持"算到一半停下来,让浏览器喘口气"(并发渲染,第 23 章),就得把一次渲染拆成可以暂停的小工作单元。Fiber 就是那个工作单元。

所以 Fiber 一个对象,同时干了三份活:

  1. 描述一个组件:它是什么类型、状态放哪、props 是多少。
  2. 串起一棵树:靠三个指针连成单链表,方便深度优先遍历。
  3. 记录工作进度:要更新什么、副作用是什么、优先级多高。

这三个身份,正好对应 FiberNode 构造函数里那三组字段(ReactFiber.js:136)。

三个指针:child / sibling / return

Fiber 树不是普通的多叉树,它是单链表。每个节点只有三个指针,来自 ReactInternalTypes.js:127

  • child:第一个子节点。上面图里 div 的第一个孩子是 span
  • sibling:右边相邻的兄弟。buttonspan 的 sibling。
  • return:父节点。注意名字不叫 parent,源码注释说得很直白(ReactInternalTypes.js:121):

The Fiber to return to after finishing processing this one. It is conceptually the same as the return address of a stack frame.

return 就是栈帧的返回地址。React 遍历整棵树,靠的不是递归(递归栈会爆),而是自己维护一个循环:进入子节点用 child,处理完往右用 sibling,到底了顺着 return 回到父节点。这套遍历在下一章的工作循环里是主角。

Fiber 对象里到底放了什么

FiberNode 构造器(ReactFiber.js:136)初始化了一长串字段,按职责分组看就不晕了:

描述组件tag(是什么类型的 fiber)、type(函数组件就是那个函数)、stateNode(组件实例或 DOM 节点)、memoizedState(组件状态,Hooks 就挂在它上面,第 3 部分)。

处理输入输出pendingProps(这次要用的 props)、memoizedProps(上次用过的 props)、updateQueue(待处理的更新队列,第 13 章)、ref

调度与副作用lanes / childLanes(优先级,第 22 章)、flags / subtreeFlags(这棵子树要干什么活,第 8、17 章)。

双缓冲alternate(指向它的另一棵树副本,下面细说)。

字段别背,知道"这些抽屉各装一类东西"就行。用到哪章,打开哪只抽屉。

双缓冲:为什么一棵树要两棵

看第 1 章的图,FiberRoot 的 current 指向一棵树。但 React 实际同时维护两棵树

  • current 树:正在页面上展示的那棵。
  • workInProgress 树:后台施工、还没上线的另一棵。

两棵树靠 alternate 字段互相指。首次施工时,createWorkInProgressReactFiber.js:325)发现 current.alternate 还是空,就新建一个配对 fiber:

workInProgress.alternate = current;
current.alternate = workInProgress;

第 352、353 行。从此两个 fiber 共用 stateNodetype 这些不变的东西,只有施工相关字段各管各的。

两棵树的好处,写在这个函数开头的注释里(ReactFiber.js:328):最多只需要两份树,一份用来算,一份用来展示。算到一半被高优先级任务打断,下次接着算也不怕污染正在展示的那份。渲染完成,commit 阶段把两个指针一换(第 17 章),旧 current 变成回收站,新树顶上。这就是"双缓冲",和显卡交换前后缓冲是同一个思路。

tag:同一种结构,干不同的活

Fiber 数据结构只有一种,但干活的类型不同,靠 tag 区分。常见几个:

tag对应的东西
HostRoot根节点整棵树的根,第 1 章见过
FunctionComponent函数组件调函数,走 Hooks(第 6、9 章)
HostComponentDOM 元素对应一个真实 DOM 节点(第 6、18 章)
HostText纯文本"你好" 这种文本节点
Fragment / Suspense / Offscreen特殊组件各有各的渲染逻辑

tag 决定了两件大事:渲染阶段怎么走(beginWork 按 tag 分派,第 6 章),以及 commit 阶段怎么落 DOM(第 17 章)。现在只需要知道:Fiber 是通用容器,tag 是贴在容器上的工种标签

动手实验

  1. DevTools 看真实 Fiber 树:打开一个 React 19 页面,DevTools → Components,选一个节点,右侧的 props/状态就是那个 fiber 的 memoizedPropsmemoizedState。选到组件,能直接看到 Hooks 列表。前言说的"把书里的图搬到眼前",这就是最直接的一步。
  2. 扒开一个 fiber:在 React 项目里随便给一个组件加断点,或者用 console.log(document.querySelector('#root')._reactRootContainer)(不推荐,生产下没有)。更简单的办法:DevTools 里对组件右键 → "查看源代码"附近的调试入口。重点看 childsiblingreturn 三个指针是不是真的像上图那样连成链。
  3. 双缓冲验证:在任意组件 render 里打一行 console.log,观察两次相邻渲染之间,某个 fiber 的 alternate 字段是否指向一棵"长得几乎一样"的另一棵树。

小结

  • 元素是图纸,Fiber 是按图施工的结构,两者一一对应。
  • Fiber 是可中断的工作单元,这是 React 16 引入它的初衷。
  • 树用单链表组织,child / sibling / return 三指针,return 就是栈帧返回地址。
  • 双缓冲:current 树展示,workInProgress 树施工,alternate 互指,commit 时交换。
  • 同一种结构靠 tag 区分工种,tag 决定渲染和提交怎么走。
  • 下一步:有了数据结构,看它怎么动起来。第 4 章给全书最关键的全局地图。