第 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 一个对象,同时干了三份活:
- 描述一个组件:它是什么类型、状态放哪、props 是多少。
- 串起一棵树:靠三个指针连成单链表,方便深度优先遍历。
- 记录工作进度:要更新什么、副作用是什么、优先级多高。
这三个身份,正好对应 FiberNode 构造函数里那三组字段(ReactFiber.js:136)。
三个指针:child / sibling / return
Fiber 树不是普通的多叉树,它是单链表。每个节点只有三个指针,来自 ReactInternalTypes.js:127:
child:第一个子节点。上面图里div的第一个孩子是span。sibling:右边相邻的兄弟。button是span的 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 字段互相指。首次施工时,createWorkInProgress(ReactFiber.js:325)发现 current.alternate 还是空,就新建一个配对 fiber:
workInProgress.alternate = current;
current.alternate = workInProgress;
第 352、353 行。从此两个 fiber 共用 stateNode、type 这些不变的东西,只有施工相关字段各管各的。
两棵树的好处,写在这个函数开头的注释里(ReactFiber.js:328):最多只需要两份树,一份用来算,一份用来展示。算到一半被高优先级任务打断,下次接着算也不怕污染正在展示的那份。渲染完成,commit 阶段把两个指针一换(第 17 章),旧 current 变成回收站,新树顶上。这就是"双缓冲",和显卡交换前后缓冲是同一个思路。
tag:同一种结构,干不同的活
Fiber 数据结构只有一种,但干活的类型不同,靠 tag 区分。常见几个:
| tag | 对应的东西 | 活 |
|---|---|---|
HostRoot | 根节点 | 整棵树的根,第 1 章见过 |
FunctionComponent | 函数组件 | 调函数,走 Hooks(第 6、9 章) |
HostComponent | DOM 元素 | 对应一个真实 DOM 节点(第 6、18 章) |
HostText | 纯文本 | "你好" 这种文本节点 |
Fragment / Suspense / Offscreen | 特殊组件 | 各有各的渲染逻辑 |
tag 决定了两件大事:渲染阶段怎么走(beginWork 按 tag 分派,第 6 章),以及 commit 阶段怎么落 DOM(第 17 章)。现在只需要知道:Fiber 是通用容器,tag 是贴在容器上的工种标签。
动手实验
- DevTools 看真实 Fiber 树:打开一个 React 19 页面,DevTools → Components,选一个节点,右侧的 props/状态就是那个 fiber 的
memoizedProps和memoizedState。选到组件,能直接看到 Hooks 列表。前言说的"把书里的图搬到眼前",这就是最直接的一步。 - 扒开一个 fiber:在 React 项目里随便给一个组件加断点,或者用
console.log(document.querySelector('#root')._reactRootContainer)(不推荐,生产下没有)。更简单的办法:DevTools 里对组件右键 → "查看源代码"附近的调试入口。重点看child、sibling、return三个指针是不是真的像上图那样连成链。 - 双缓冲验证:在任意组件 render 里打一行
console.log,观察两次相邻渲染之间,某个 fiber 的alternate字段是否指向一棵"长得几乎一样"的另一棵树。
小结
- 元素是图纸,Fiber 是按图施工的结构,两者一一对应。
- Fiber 是可中断的工作单元,这是 React 16 引入它的初衷。
- 树用单链表组织,
child/sibling/return三指针,return就是栈帧返回地址。 - 双缓冲:current 树展示,workInProgress 树施工,
alternate互指,commit 时交换。 - 同一种结构靠
tag区分工种,tag决定渲染和提交怎么走。 - 下一步:有了数据结构,看它怎么动起来。第 4 章给全书最关键的全局地图。