React Fiber 数据结构 / 双缓冲机制
这篇笔记不再按断点流水账组织,而是围绕 Fiber 解决的核心问题来学:
JSX
-> ReactElement
-> HostRoot update
-> render 阶段 reconcile
-> Fiber 树 / workInProgress 树
-> commit 阶段切换 root.current
-> DOM 更新完成本篇要回答三个面试里的主问题:
React 是怎么从 JSX 走到 Fiber,再走到页面更新的?
FiberNode 为什么既是节点数据结构,也是一个可调度的工作单元?
current、workInProgress、alternate和双缓冲到底怎么配合?
学习边界:
重点展开:
FiberNode 结构
JSX -> ReactElement -> Fiber 的链路
current / workInProgress / alternate
从 HostRoot 开始的双缓冲流程
createRoot / root.current / createWorkInProgress 的关键源码位置
暂不展开:
beginWork / completeWork 的完整分支
commitRoot 的完整三阶段
Lane 模型细节
Hooks updateQueue 细节
Suspense / Transition 细节一、Fiber 的整体认知:它到底是什么
Fiber 不是虚拟 DOM 本身,也不是 DOM 节点的简单包装。
一句话:
Fiber 是 React 内部表示组件节点、更新状态、副作用和调度信息的工作单元。它同时承担几类职责:
维度 | Fiber 里保存什么 | 为什么重要 |
|---|---|---|
节点身份 |
| 让 React 知道这个节点是什么 |
树结构 |
| 让 React 可以不用递归栈遍历整棵树 |
数据状态 |
| 让 render 阶段能计算新 props / state |
副作用 |
| 让 commit 阶段知道要提交什么 |
优先级 |
| 让调度器知道哪些更新该先处理 |
双缓冲 |
| 让 current 和 workInProgress 两份 Fiber 可以互相复用 |
所以面试里不要只说“Fiber 是链表树”。更完整的表达是:
Fiber 是 React 为了把渲染工作拆成可暂停、可恢复、可复用的工作单元而设计的内部数据结构。它既表示组件树里的一个节点,也保存这次更新需要的状态、优先级和副作用信息。
有了这个整体认知,下一步要把 Fiber 放回 React 主链路里:我们写的 JSX 并不会直接变成 Fiber。
二、从 JSX 到 Fiber:ReactElement 只是输入
我们写的入口通常是:
createRoot(document.getElementById('root')).render(<App />);准确链路不是“JSX 直接创建 Fiber”,而是:
JSX 源码
-> 构建工具转换成 jsx/jsxs/jsxDEV 调用
-> 浏览器运行这些函数,创建 ReactElement
-> root.render(element) 把 element 放进 HostRoot updateQueue
-> render 阶段处理 HostRoot update
-> reconcileChildren 把 ReactElement 创建或复用成 FiberJSX 先被转换成运行时调用
在 Vite + React 项目里,JSX 语法由构建工具转换。开发或构建时,@vitejs/plugin-react 参与 transform pipeline,底层 transformer 可能随版本不同使用 Babel、esbuild、SWC 或 Oxc。
生产形态大致是:
import {jsx as _jsx} from 'react/jsx-runtime';
createRoot(document.getElementById('root')).render(_jsx(App, {}));开发形态为了报错定位和 Fast Refresh,可能是:
import {jsxDEV as _jsxDEV} from 'react/jsx-dev-runtime';
createRoot(document.getElementById('root')).render(
_jsxDEV(App, {}, undefined, false, undefined, this)
);关键点:
编译 / 转换阶段:<App /> 变成 jsx(App, {}) 这种普通函数调用。
运行阶段:浏览器执行 jsx(App, {}),React 才创建 ReactElement 对象。ReactElement 的简化结构:
function ReactElement(type, key, props, owner) {
return {
$$typeof: REACT_ELEMENT_TYPE,
type,
key,
props,
_owner: owner,
};
}ReactElement 只是描述“想渲染什么”:
字段 | 含义 |
|---|---|
| 组件函数、DOM 标签字符串或特殊 React 类型 |
| 列表 diff 的稳定标识 |
| JSX 上的属性和 children |
| 标识这是 ReactElement |
ReactElement 通过 HostRoot update 进入 render
root.render(<App />) 不是立刻创建 App Fiber,而是先把 <App /> 作为一次根更新放到 HostRoot 上:
ReactDOMRoot.prototype.render(children)
-> updateContainer(children, root, null, null)
-> updateContainerImpl(rootFiber, lane, element, ...)
-> createUpdate(lane)
-> update.payload = {element}
-> enqueueUpdate(rootFiber, update, lane)到 render 阶段处理 HostRoot 时,React 才取出这个 element:
HostRoot updateQueue
-> element: <App />
-> reconcileChildren
-> createFiberFromElement(<App />)
-> createFiberFromTypeAndProps(App, ...)
-> App Fiber源码位置:
步骤 | 关键文件 | 关键位置 |
|---|---|---|
JSX runtime 出口 |
| 导出 |
ReactElement 创建 |
|
|
|
|
|
|
|
|
root update 入队 |
|
|
Element 创建 Fiber |
|
|
child diff 入口 |
|
|
到这里可以先形成一个稳定判断:
ReactElement 是 render 的输入。
Fiber 是 React 在 render / reconcile 阶段创建或复用出来的内部工作节点。理解了输入链路后,再看 FiberNode 自己的字段就不会散。
三、FiberNode 结构:字段按职责记
FiberNode 构造函数在:
packages/react-reconciler/src/ReactFiber.js关键位置:
FiberNode 约 139 行
createWorkInProgress 约 328 行
createHostRootFiber 约 534 行
createFiberFromTypeAndProps 约 559 行
createFiberFromElement 约 741 行简化版结构:
FiberNode
├─ 身份信息:tag / key / elementType / type / stateNode
├─ 树结构:return / child / sibling / index
├─ 数据状态:pendingProps / memoizedProps / memoizedState / updateQueue
├─ 副作用:flags / subtreeFlags / deletions
├─ 优先级:lanes / childLanes
└─ 双缓冲:alternate常用字段表:
字段 | 含义 |
|---|---|
| React 内部的 Fiber 节点类型 |
| 列表 diff 时用于稳定识别节点 |
| JSX 输入时的原始 type |
| React 当前真正处理的组件函数、DOM 标签或特殊类型 |
| Fiber 关联的真实实例 |
| 父 Fiber |
| 第一个子 Fiber |
| 下一个兄弟 Fiber |
| 本次 render 准备使用的新 props |
| 上一次提交后的 props |
| 上一次提交后的 state;函数组件里是 Hooks 链表入口 |
| 待处理更新队列或 effect 队列 |
| 当前 Fiber 自身的副作用标记 |
| 子树里的副作用标记汇总 |
| 本轮需要删除的子 Fiber |
| 当前 Fiber 自身待处理更新的优先级集合 |
| 子树中待处理更新的优先级集合 |
| current 和 workInProgress 两个版本之间的互相指针 |
简化版源码:
function FiberNode(tag, pendingProps, key, mode) {
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null;
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.memoizedState = null;
this.updateQueue = null;
this.flags = NoFlags;
this.subtreeFlags = NoFlags;
this.deletions = null;
this.lanes = NoLanes;
this.childLanes = NoLanes;
this.alternate = null;
}这些字段里,面试最容易追问的是 tag、type、stateNode,因为它们共同回答“这个 Fiber 到底代表什么”。
四、tag / type / stateNode:一个 Fiber 的身份三件套
先用一句话区分:
tag = React 内部怎么看这个 Fiber
type = JSX 原始身份或 React 解析后的组件 / 标签
stateNode = 这个 Fiber 连接到的真实实例tag:React 内部节点类型
tag 是 React 内部的节点类型枚举,定义在:
packages/react-reconciler/src/ReactWorkTags.js高频枚举:
tag 名称 | 数值 | 常见来源 |
|---|---|---|
|
| 函数组件 |
|
| class 组件 |
|
| React root |
|
| Portal |
|
| DOM 节点,如 |
|
| 文本节点 |
|
|
|
|
| Context Provider |
|
|
|
|
| Profiler |
|
| Suspense |
|
|
|
|
| 简化后的 memo 组件 |
|
|
|
|
| Offscreen |
|
| React 19.2 |
type:组件函数、DOM 标签或特殊类型
type 通常来自 ReactElement 的 element.type,React 会根据它推导 fiber.tag。
JSX |
|
|
|---|---|---|
|
|
|
|
|
|
文本 | 文本相关 |
|
|
|
|
|
|
|
| memo 包装对象 |
|
| forwardRef 包装对象 |
|
elementType 和 type 大多数时候相同。遇到 lazy、热更新、memo、forwardRef 等包装类型时,两者可能不同:
elementType = JSX 输入时的原始 type
type = React 当前解析后真正处理的 typestateNode:连接宿主实例或组件实例
stateNode 是 Fiber 关联到的真实实例:
Fiber tag |
|
|---|---|
|
|
| 真实 DOM 节点,如 |
| 真实文本节点 |
| class 组件实例 |
|
|
| 通常没有真实实例 |
函数组件的 stateNode 是 null,因为函数组件没有实例。函数组件的 Hooks 状态挂在:
fiber.memoizedState身份三件套讲完后,还需要理解 Fiber 为什么能支撑可中断遍历:它不是靠 children 数组递归走树,而是靠三根指针。
五、Fiber 树结构:child / sibling / return
Fiber 树不是用 children 数组保存子节点,而是用链表指针:
child -> 第一个子 Fiber
sibling -> 下一个兄弟 Fiber
return -> 父 Fiber示例 JSX:
<App>
<Header />
<Main />
<Footer />
</App>对应 Fiber 链接:
App.child -> Header
Header.sibling -> Main
Main.sibling -> Footer
Header.return -> App
Main.return -> App
Footer.return -> App这套结构的意义:
React 可以把整棵组件树拆成一个个 Fiber 工作单元。
处理一个 Fiber 后,可以根据 child / sibling / return 找到下一个工作单元。
所以 render 阶段不必依赖不可暂停的递归调用栈。不过,Fiber 能被拆成工作单元只是基础。真正避免污染已提交页面状态的,是 current / workInProgress 两棵树的双缓冲。
六、current / workInProgress / alternate:双缓冲的核心关系
先把三个词放在一起:
名称 | 含义 |
|---|---|
| 当前已经提交,和页面一致的 Fiber 树 |
| 本次 render 阶段正在计算的新 Fiber 树 |
| 同一个逻辑节点的 current Fiber 与 WIP Fiber 之间的互相指针 |
简化理解:
current = 已经交付的正式稿
workInProgress = 正在修改的草稿
commit = 草稿通过,替换正式稿
alternate = 正式稿和草稿之间的互相指针同一个组件最多保留两份 Fiber,来回复用:
current Fiber <---- alternate ----> workInProgress Fiber关键不变量:
current !== workInProgress
current.alternate === workInProgress
workInProgress.alternate === current角色切换:
第 1 次更新:
A 是 current
B 是 workInProgress
commit 后,B 变成新的 current
第 2 次更新:
B 是 current
A 被复用为 workInProgress
commit 后,A 变成新的 current为什么需要双缓冲?
问题 | 双缓冲怎么解决 |
|---|---|
render 阶段可能很长 | 在 WIP 树上计算,不直接污染 current |
render 阶段可能被中断或重试 | 丢弃或重做 WIP,不影响已提交页面 |
commit 需要一致性 | render 完成后一次性提交 finishedWork |
每次都新建整棵树成本高 | current 和 WIP 两份 Fiber 交替复用 |
所以双缓冲不是“两个 DOM 树”,而是:
两份 Fiber 树在内存里交替承担 current 和 workInProgress 角色。接下来要把这件事从根节点完整走一遍,因为 root.current 的切换才是双缓冲真正生效的位置。
七、从根节点开始:root.current 如何完成整棵树切换
createRoot(container) 会创建两个关键对象:
FiberRoot
HostRoot Fiber它们互相连接:
FiberRoot.current -> HostRoot Fiber
HostRoot Fiber.stateNode -> FiberRoot源码链路:
createRoot(container)
-> createContainer(container, ConcurrentRoot, ...)
-> createFiberRoot(...)
-> createHostRootFiber(...)createFiberRoot 的简化源码:
function createFiberRoot(containerInfo, tag, hydrate) {
const root = new FiberRootNode(containerInfo, tag, hydrate);
const uninitializedFiber = createHostRootFiber(tag);
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
initializeUpdateQueue(uninitializedFiber);
return root;
}对应源码位置:
packages/react-reconciler/src/ReactFiberRoot.js
createFiberRoot 约 158 行
root.current = uninitializedFiber
uninitializedFiber.stateNode 约 212-213 行初次 createRoot 后
此时只有 HostRoot current,还没有 <App /> 对应的 Fiber:
FiberRoot
current
|
v
HostRoot currentroot.render 后进入 render
root.render(<App />) 会调度一次 HostRoot 更新。render 开始时,React 基于:
root.current创建或复用:
HostRoot workInProgress随后在 HostRoot WIP 上处理 updateQueue,拿到 <App />,再 reconcile 出 App Fiber 和后续子 Fiber:
FiberRoot.current
|
v
HostRoot A current <----alternate----> HostRoot B WIP
child -> App B WIP -> div B WIPrender 完成后进入 commit
render 阶段完成后,得到:
finishedWork = 已经算完的 HostRoot workInProgress Fibercommit 成功后,React 进行根指针切换:
root.current = finishedWork;切换后:
FiberRoot.current
|
v
HostRoot B current
child -> App B current -> div B current
HostRoot A / App A 留作下一次更新的 alternate完整心智图:
更新前:
FiberRoot.current
|
v
HostRoot A current
child -> App A current -> div A current
更新中:
FiberRoot.current
|
v
HostRoot A current <----alternate----> HostRoot B WIP
child -> App A current child -> App B WIP
^ ^
| alternate | alternate
v v
App B WIP App A current
commit 后:
FiberRoot.current
|
v
HostRoot B current
child -> App B current -> div B current
HostRoot A / App A 留作下一次更新的 alternate这里最重要的结论:
WIP 变成 current 不是某个组件自己完成的。
commit 阶段通过 root.current = finishedWork,把整棵 WIP 树切换成新的 current 树。那 WIP Fiber 是如何从 current Fiber 创建或复用出来的?入口就是 createWorkInProgress。
八、createWorkInProgress:创建或复用 WIP Fiber
createWorkInProgress 在:
packages/react-reconciler/src/ReactFiber.js关键位置:
createWorkInProgress(current, pendingProps) 约 328 行简化版源码:
function createWorkInProgress(current, pendingProps) {
let workInProgress = current.alternate;
if (workInProgress === null) {
workInProgress = createFiber(
current.tag,
pendingProps,
current.key,
current.mode
);
workInProgress.elementType = current.elementType;
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode;
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
workInProgress.pendingProps = pendingProps;
workInProgress.type = current.type;
workInProgress.flags = NoFlags;
workInProgress.subtreeFlags = NoFlags;
workInProgress.deletions = null;
}
workInProgress.child = current.child;
workInProgress.memoizedProps = current.memoizedProps;
workInProgress.memoizedState = current.memoizedState;
workInProgress.updateQueue = current.updateQueue;
workInProgress.lanes = current.lanes;
workInProgress.childLanes = current.childLanes;
return workInProgress;
}这段源码对应三件事:
分支 | 行为 | 含义 |
|---|---|---|
| 新建 WIP Fiber | 第一次需要第二份 Fiber |
| 复用旧 WIP Fiber | 第二次及以后两份 Fiber 交替复用 |
拷贝字段 | 复制 | WIP 基于 current 的已提交状态继续计算 |
特别注意:
flags / subtreeFlags / deletions 会被重置。
memoizedProps / memoizedState / updateQueue 会从 current 复制。
child 先指向 current.child,后续 beginWork / reconcileChildren 再按需要复用或替换。所以 createWorkInProgress 不是“把 current 深拷贝成一整棵树”。它是从根部开始,按 render 遍历过程逐步为每个需要处理的 Fiber 创建或复用 WIP。
这也解释了为什么 debug 时在某个组件的 createWorkInProgress 断点里,能观察到同一个逻辑组件的两份 Fiber。
九、面试问答:把 Fiber 讲成一条线
Q1:React 是怎么从 JSX 走到浏览器页面更新的?
回答要点:
JSX 不是浏览器原生语法,会先被 Vite / Babel / SWC 等构建工具转换成
jsx、jsxs或jsxDEV函数调用。浏览器执行这些函数调用时,React 创建 ReactElement。ReactElement 是描述 UI 的普通对象,核心字段包括
type、key、props。createRoot(container)会创建 FiberRoot 和 HostRoot Fiber,root.render(<App />)会把 ReactElement 放进 HostRoot 的 updateQueue。render 阶段从 HostRoot 开始处理 updateQueue,对 ReactElement 做 reconcile,创建或复用 Fiber 节点,形成 workInProgress Fiber 树。
commit 阶段把 render 阶段计算出的 DOM 变更和副作用提交到浏览器,并把
root.current切换到新的 Fiber 树。
简短表达:
JSX 先被构建工具变成 JSX runtime 函数调用,运行时生成 ReactElement;ReactElement 进入 HostRoot 的 updateQueue 后,在 render 阶段被 reconcile 成 Fiber 树;commit 阶段才把 Fiber 上计算出的变更真正提交到 DOM。
Q2:ReactElement 和 Fiber 有什么区别?
回答要点:
ReactElement 是不可变的 UI 描述,来自 JSX runtime。
Fiber 是 React 内部的工作单元,保存组件身份、树结构、状态、updateQueue、副作用 flags、lanes 和 alternate。
ReactElement 告诉 React “想渲染什么”,Fiber 承载“如何计算、如何更新、如何提交”。
ReactElement 本身没有调度能力;可中断、恢复、优先级和副作用收集都发生在 Fiber 体系里。
简短表达:
ReactElement 是输入,Fiber 是 React 为了调度和更新创建的内部工作结构。Element 描述 UI,Fiber 承载状态、更新队列、副作用和优先级。
Q3:Fiber 到底是什么,为什么 React 需要它?
回答要点:
Fiber 是 React 内部表示组件节点和更新工作的数据结构。
旧的递归更新模型一旦开始就很难暂停,长任务容易阻塞浏览器主线程。
Fiber 把组件树拆成一个个可处理的工作单元,通过
child、sibling、return链接起来。React 可以在 render 阶段逐个处理 Fiber,必要时让出主线程,后续再继续。
Fiber 还保存 flags、lanes、updateQueue 等信息,让调度、diff、提交有统一载体。
简短表达:
Fiber 是 React 为了把渲染工作拆小、可调度、可恢复而设计的内部工作单元。它既表示节点,也保存本次更新需要的状态、优先级和副作用。
Q4:tag、type、stateNode 分别是什么?
回答要点:
tag是 React 内部 Fiber 节点类型,例如FunctionComponent、HostComponent、HostRoot。type是 React 当前真正处理的组件函数、DOM 标签字符串或特殊 React 类型。stateNode是 Fiber 关联的真实实例;HostComponent 是 DOM 节点,ClassComponent 是 class 实例,FunctionComponent 是null。
简短表达:
tag说明 React 内部如何分发处理这个 Fiber,type说明它对应哪个组件或宿主标签,stateNode连接真实实例。函数组件没有实例,所以stateNode是null。
Q5:current、workInProgress、alternate 是什么关系?
回答要点:
current是当前已经提交、和页面状态一致的 Fiber 树。workInProgress是本次 render 阶段正在计算的新 Fiber 树。同一个组件的 current Fiber 和 WIP Fiber 通过
alternate互相指向。React 不直接修改 current,而是在 WIP 上计算新状态、新 children 和副作用。
commit 成功后,
root.current = finishedWork,WIP 树变成新的 current 树。
简短表达:
current 是正式稿,workInProgress 是草稿,alternate 是两份 Fiber 的互相指针。React 在草稿上计算,commit 成功后再把草稿切成正式稿。
Q6:双缓冲机制解决了什么问题?
回答要点:
避免 render 阶段直接污染当前页面对应的稳定 Fiber 树。
支持 render 阶段被中断、丢弃或重试。
支持在内存中先完整计算下一棵树,commit 阶段再一次性提交。
避免每次更新都重新创建整棵树,current 和 WIP 两份 Fiber 可以复用。
简短表达:
双缓冲让 React 可以一边保留当前已经显示的 Fiber 树,一边在另一棵 WIP 树上计算下一次更新。只有 render 成功后才 commit,所以它是并发渲染和稳定提交的基础。
Q7:为什么说 Fiber 支持可中断渲染?
回答要点:
Fiber 把组件树拆成一个个工作单元。
每个 Fiber 都能通过
child、sibling、return找到下一个工作单元。render 阶段在 workInProgress 树上计算,不直接提交 DOM。
如果当前任务优先级不够或浏览器需要让出主线程,React 可以暂停 WIP 计算,之后继续或重试。
最终只有完成的
finishedWork才进入 commit。
简短表达:
Fiber 的链表树结构让 React 可以逐个处理工作单元;双缓冲让这些计算发生在 WIP 树上。因为 render 不直接改 DOM,React 才能中断、恢复或丢弃本次计算。
一段完整表达草稿
如果面试官问 React 从 JSX 到页面更新的整体链路,我会先讲主线:
JSX 会被构建工具转换成 JSX runtime 调用,运行时生成 ReactElement;
root.render把这个 ReactElement 作为一次 HostRoot update 入队;render 阶段从 HostRoot 开始 reconcile,把 ReactElement 创建或复用成 Fiber 节点,形成 workInProgress 树;commit 阶段再根据 Fiber 上的 flags 把变更提交到 DOM,并切换root.current。
然后补 Fiber 的价值:
Fiber 是这条链路里的核心内部数据结构。它不仅表示 UI 节点,还保存组件类型、父子兄弟关系、props、state、更新队列、副作用标记和更新优先级。React 更新时不会直接修改已经提交的 current Fiber 树,而是基于 current 创建或复用 workInProgress Fiber 树,在 WIP 上完成 render 计算。current 和 WIP 通过
alternate互相指向。render 成功后,commit 阶段把 WIP 提交到页面,并让 WIP 成为新的 current。这样 React 可以避免中途渲染失败或被打断时污染当前页面对应的稳定 Fiber 数据。


评论
还没有评论
来发表第一条评论吧!