很多人第一次接触 Fiber,都是在面试题里听到一句话: “Fiber 让 React 变成了可中断渲染。”这句话不算错,但也只说对了一半。
如果只把 Fiber 理解成“性能优化”,其实很容易越学越糊涂。因为它解决的核心问题,不是单纯让一次渲染跑得更快,而是让 React 有能力决定: 哪些工作该先做,哪些工作可以等一等,哪些工作做到一半可以停下来,甚至推倒重来。
Fiber 随 React 16 在 2017 年正式落地。到 React 18 之后我们熟悉的并发渲染、startTransition、更细粒度的调度能力,本质上都建立在 Fiber 这套执行模型之上。
先看老问题: 为什么 React 需要重写 Reconciler
早期 React 的 reconciler 通常被称为 Stack Reconciler。它的思路很直接: 组件更新后,递归地往下走,算出新树,再把变更提交出去。
这个模型在简单场景下没什么问题,但它有一个天然短板: 一旦开始递归,中途很难停。
这在 UI 编程里是个真实问题。因为界面更新并不是“越快全部做完越好”,而是“先保证用户手上的操作别卡住”。
比如下面这两类更新,紧急程度其实完全不同:
- 用户正在输入框里打字
- 页面某个大列表因为筛选条件变化,需要重新渲染很多项
如果框架把两者都当成“立刻完整做完”的工作,主线程就可能被长任务占住,输入、动画、滚动都会变得不跟手。
Fiber 的目标,就是把 React 的渲染过程从“一次性跑到底”,改成“拆成很多可管理的小单元”。
Fiber 到底是什么
先给一个不那么绕的定义:
Fiber 是 React 内部用来表示“一个组件及其待处理工作”的数据结构,也是调度和协调更新的基本单位。
它不是你在业务代码里会直接接触到的东西。你写的是 JSX 和组件,React 在内部会把这些内容组织成一棵 Fiber 树。
一个 Fiber 节点通常会保存这些信息:
- 这个节点对应什么类型的组件
- 它的
key - 它的父节点、子节点、兄弟节点是谁
- 上一次已经完成的 props/state 是什么
- 这次待处理的更新是什么
- 它对应的宿主实例是什么,比如某个 DOM 节点
- 这次更新属于哪些优先级通道,也就是
lanes
很多老文章会提到 expirationTime 或 pendingWorkPriority。这些说法对应的是较早期的 Fiber 实现。现代 React 源码里,优先级模型更接近 lanes。可以把它粗略理解成“一组位标记”,React 用它来表达更新的紧急程度、可否合并,以及哪些更新需要一起完成。
如果把源码里的 FiberNode 拆开看
在 packages/react-reconciler/src/ReactFiber.js 里,React 定义了 Fiber 节点本身。你不用死记全部字段,但最好知道它大概按什么思路组织。
可以把一个 Fiber 节点粗略理解成下面这样:
type FiberNode = {
tag: WorkTag;
key: null | string;
elementType: any;
type: any;
stateNode: any;
return: Fiber | null;
child: Fiber | null;
sibling: Fiber | null;
index: number;
pendingProps: any;
memoizedProps: any;
memoizedState: any;
updateQueue: mixed;
flags: Flags;
subtreeFlags: Flags;
deletions: Array<Fiber> | null;
lanes: Lanes;
childLanes: Lanes;
alternate: Fiber | null;
}
这里面最值得先记住的是几组字段:
tag: 这个节点是什么类型,比如函数组件、类组件、宿主节点、根节点elementType和type: 分别描述“用户写的元素类型”和“最终用来工作的类型信息”stateNode: 指向真正挂着的东西。对 DOM 节点来说通常是实际 DOM;对类组件来说是组件实例;对根节点来说会关联到FiberRootreturn、child、sibling: 这组三个指针把整棵 Fiber 树串起来pendingProps、memoizedProps、memoizedState: 分别表示这轮要处理的输入,以及上一次已经完成的结果flags和subtreeFlags: 标记当前节点和子树在 commit 阶段需要做什么lanes和childLanes: 当前节点以及它的子树上,分别还挂着哪些优先级的工作alternate: 指向另一棵树上的对应节点,也就是current和workInProgress之间的连接
如果你第一次读源码,最容易混淆的是 pendingProps 和 memoizedProps。一个表示“这轮准备处理什么”,一个表示“上轮已经确认了什么”。React 是否需要继续往下算,经常就跟这两者的差异,以及当前 lanes 有没有命中有关。
Fiber 树本身不是传统的“每个节点挂一个 children 数组”的结构,而更像一棵基于链表关系连接起来的树:
graph TD
P["Parent Fiber"]
A["Child Fiber A"]
B["Child Fiber B"]
P -->|child| A
A -->|sibling| B
A -.->|return| P
B -.->|return| P
这么设计有个实际好处: React 不必完全依赖 JavaScript 调用栈去遍历组件树,而是能把“下一步处理谁”这件事掌握在自己手里。
从 JSX 到 Fiber 树,中间发生了什么
站在业务代码视角,我们写的是组件嵌套:
function App() {
return (
<main>
<Header />
<List />
</main>
);
}
React 内部真正处理时,会把它们组织成更接近下面这样的结构:
graph TD
A["App(FunctionComponent)"]
B["main(HostComponent)"]
C["Header(FunctionComponent)"]
D["List(FunctionComponent)"]
A -->|child| B
B -->|child| C
C -->|sibling| D
这里要注意两点:
- 组件节点会有对应的 Fiber,原生 DOM 标签同样也会有对应的 Fiber
- “组件树”和“Fiber 树”不是完全不同的两套东西,Fiber 更像是 React 为这些节点加上的运行时外壳
所以很多时候我们说“遍历组件树”,在源码语境里更准确地讲,其实是在遍历 Fiber 树。
current 和 workInProgress: React 的“双缓冲”
理解 Fiber,绕不开另一个关键点: React 内部通常会同时维护两棵互相关联的树。
current表示当前已经显示在页面上的 Fiber 树workInProgress表示这次更新过程中正在构建的新树
当一次更新开始时,React 会基于当前树去创建或复用对应的 workInProgress 节点。两个版本之间通过 alternate 指针互相引用。
graph LR
C["current Fiber tree"] -. alternate .- W["workInProgress Fiber tree"]
等整棵新树准备完成之后,React 再把 workInProgress 切成新的 current。这就是很多文章里说的“双缓冲”。
这件事很重要,因为它意味着 React 可以在“还没准备好之前”只在内存里推演,不急着把半成品直接改到 DOM 上。
顺着源码再看一步,createWorkInProgress 这类函数做的事情,也不是每次都新建一整棵树。React 会尽量复用 alternate 对应的节点,只把这轮需要更新的字段重置或覆盖掉。这样做的目的很现实: 降低重复分配对象的成本,也减少垃圾回收压力。
一次更新在 Fiber 里是怎么走的
我们平时写的 setState、父组件重新渲染、上下文变化,最终都会变成一次更新任务。
简化来看,一次更新大致会经过下面这条路径:
graph TD
U["setState / props 变化 / startTransition"] --> L["分配 lanes"]
L --> W["创建或复用 workInProgress"]
W --> R["Render 阶段: 遍历 Fiber 树"]
R --> Y{"需要让出主线程吗?"}
Y -- "需要" --> R
Y -- "不需要,且本轮工作完成" --> C["Commit 阶段"]
C --> B["Before Mutation"]
B --> M["Mutation: 应用 DOM 变更"]
M --> LA["Layout: useLayoutEffect 等"]
LA --> P["Passive Effects: 稍后刷新 useEffect"]
这里最容易记住的,其实就两句话:
1. Render 阶段负责“算”
Render 阶段会从根节点开始遍历 Fiber 树,做的事情包括:
- 根据新输入重新执行组件
- 对比新旧子树,决定哪些节点可以复用
- 记录本次需要提交的变更
这个阶段的关键特征是: 可以被打断,可以恢复,也可以直接重来。
所以“并发渲染”首先是一个调度能力,不是说 React 突然在浏览器里开了多线程。
2. Commit 阶段负责“改”
当 Render 阶段已经得到一棵可提交的新树后,React 才会进入 Commit 阶段,把真实变更应用到宿主环境里,比如 DOM。
Commit 一旦开始,默认就不会再被打断。原因也很直白: DOM 改到一半停下来,界面就会处在不一致状态。
这也是为什么很多资料里会反复强调一句话:
Fiber 让 Render 变得可中断,但 Commit 仍然是同步完成的。
beginWork 和 completeWork,可以怎么理解
如果你去看 React reconciler 的源码,经常会看到两个名字: beginWork 和 completeWork。
不必一上来就抠实现细节,先用职责去记就够了:
beginWork更像“往下看”,决定当前节点和它的子节点这轮该怎么处理completeWork更像“往上收”,在子节点处理完后,把当前节点需要提交的信息归并回来
这套“下探再回收”的过程,最终会把整棵 workInProgress 树准备好。
从源码看一次 work loop
如果把 Fiber 运行过程再翻译成源码里的几个关键函数,主线会更清楚:
performUnitOfWorkbeginWorkcompleteUnitOfWorkcompleteWork
它的大体节奏可以画成这样:
graph TD
A["performUnitOfWork(current)"] --> B["beginWork"]
B -->|返回 child| C["进入子节点"]
B -->|返回 null| D["completeUnitOfWork"]
D --> E{"有 sibling 吗?"}
E -- "有" --> F["切到 sibling"]
E -- "没有" --> G["回到 return 节点"]
G --> D
如果只说职责:
performUnitOfWork负责驱动“处理一个 Fiber”beginWork负责决定这个 Fiber 需不需要重新计算,以及它的子节点怎么生成或复用completeWork负责在子节点都处理完后,为当前节点收尾,比如准备宿主节点相关信息completeUnitOfWork则负责把“回溯”这件事串起来,决定接下来去兄弟节点,还是继续往父节点退
你也可以把它想成一个手写遍历器,而不是依赖 JS 调用栈的递归。
很多文章会把 Fiber 描述成“把递归改成链表”,这句话不够精确,但方向是对的。更本质的变化是: React 不再把整棵树的执行控制权交给函数调用栈,而是自己维护一个 workInProgress 指针,一步一步推进。
用伪代码表示,它的心智模型大概像这样:
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
当然,真实源码里不会这么简单,而且 performUnitOfWork 也不是一个“简单返回下一个节点”的玩具函数。这里的伪代码只是为了说明: React 确实是在显式推进当前工作指针,而不是把整棵树一次性递归到底。
React 为什么能“暂停”,又为什么不是多线程
Fiber 最容易被误解的地方,就是大家会把“可中断渲染”听成“React 在后台另起线程偷偷算 UI”。
浏览器里的 React 依然主要跑在主线程上。它之所以能暂停,本质上是因为:
- 渲染工作被拆成了一个个 Fiber 单元
- React 自己掌握遍历节奏,而不是被递归一口气压到底
- 在并发模式相关的工作循环里,React 会周期性判断当前是否应该让出执行权
也就是说,Fiber 提供的是“可以切片”的基础,Scheduler 提供的是“什么时候该停一下”的判断。两者配合,才有了我们后来看到的并发更新体验。
这也是为什么说 Fiber 是并发特性的地基,而不是并发特性的全部。
Fiber 和并发渲染,到底是什么关系
一个很常见的误解是: “有了 Fiber,React 就是异步渲染了。”
这话不严谨。
更准确的说法是:
- Fiber 让 React 拥有了可调度的渲染模型
- 并发渲染是这套模型在现代 React 中的一种能力体现
- 它的重点是“可中断、可插队、可放弃过时结果”,不是“后台偷偷算完”
startTransition 就是一个很典型的例子:
startTransition(() => {
setTab(nextTab);
});
这不是在说“这个更新一定更快”,而是在告诉 React: 这次更新没那么急,如果用户此时还有更重要的交互,比如继续输入、继续点击,那么优先保证那些更紧急的更新。
React 官方文档也明确提到,Transition 更新可以被其他状态更新打断,之后再重新开始。
为什么 key 在 Fiber 里这么重要
很多人把 key 只当成“消除列表 warning 的语法要求”,其实它更重要的作用,是帮助 React 判断一个 Fiber 能不能复用。
在同一层级下,React 会结合元素类型和 key 来判断身份。身份稳定,原来的 Fiber 以及它关联的状态就有机会被保留下来;身份变了,旧节点就会被卸载,新节点会重新挂载。
这也是为什么下面两种写法,效果会完全不同:
- 只是改 props,组件状态通常会保留
- 改了
key,组件通常会被当成一个全新的实例
所以从 Fiber 的角度看,key 本质上是在参与“节点身份识别”,而不只是给 diff 算法打辅助。
如果把视角再往源码里压一层,在 ReactChildFiber.js 这类文件里,React 处理子节点时并不是无脑双层遍历。
数组 diff 的常见路径通常是这样:
- 先按顺序尝试复用“当前位置上的旧节点”
- 一旦顺序对不上,再把剩余旧节点组织成可查找结构
- 继续根据
key和类型去找还能不能复用 - 复用不了的就创建新 Fiber,旧的则标记删除
这里 key 的价值一下就具体了: 它不是给 React 一个“优化提示”,而是在告诉 React “这个节点在新旧两次渲染之间是不是同一个人”。
如果你见过源码里的 lastPlacedIndex,它解决的也是类似问题: 某个节点虽然还能复用,但它在新列表里的相对位置已经变了,那么 commit 阶段就需要把对应的移动或插入操作做出来。
Fiber 为什么有时能整片跳过不算
实际项目里我们经常会观察到一种现象: 父组件更新了,但某些子树并没有真的重新走到底。
这背后不是单一机制,而是几件事叠在一起:
- 当前 Fiber 的输入没有变化,或者没有命中这轮需要处理的 lanes
- 子树上也没有更高优先级的待处理工作
- 上下文等依赖没有发生会影响这棵子树的变化
在这些条件满足时,React 可以在 beginWork 阶段直接 bailout,也就是跳过这棵已经完成过、并且当前看来不需要重算的子树。
这也是 childLanes 存在的意义之一。当前节点自己也许没有更新,但它的孩子可能有;如果连 childLanes 都没有命中,那 React 就更有把握直接跳过。
所以从源码视角看,“跳过渲染”不是一个魔法优化项,而是 Fiber 节点把上下文、状态、优先级信息都挂在树上之后,自然获得的判断能力。
Commit 阶段到底提交了什么
前面我们说过,Render 阶段主要是在“算”,Commit 阶段才是真正“改”。如果再拆细一点,Commit 通常会被分成几个时机:
- Before Mutation
- Mutation
- Layout
- Passive Effects 刷新
它们可以粗略理解成:
- Before Mutation: DOM 真正变更前,先做一些提交前准备
- Mutation: 执行插入、删除、更新 DOM 等宿主操作
- Layout: 触发
useLayoutEffect相关逻辑,此时 DOM 已经是最新的 - Passive Effects: 之后再异步刷新
useEffect
源码层面还有一个很容易让人看旧资料时绕进去的点:
很多早期 Fiber 文章会强调 “React 会把有副作用的节点串成一条 effect list”。这个说法在历史版本里非常重要,但如果你看现代源码,会更常看到的是 flags、subtreeFlags 以及围绕它们的子树遍历逻辑。
换句话说,今天去理解 Commit 阶段,更稳妥的方式不是死记某个历史字段名,而是把握住这个核心事实:
- Render 阶段负责给 Fiber 节点打标记
- Commit 阶段根据这些标记,有选择地遍历并执行真实副作用
Fiber 和 Hooks,是怎么接上的
如果你是从业务代码出发学习 Fiber,另一个很自然的问题是: Hooks 跟这套东西怎么连起来的?
可以先抓住一个足够用的版本:
- 函数组件对应的 Fiber 会保存自己的
memoizedState - 这个
memoizedState会串起当前组件上的 Hooks 状态 - 当你调用
setState时,本质上是在对应 Hook 的更新队列里塞进一个 update - React 再根据这个 update 的 lane,把工作调度回对应 Fiber 所在的那棵树上
所以 Hooks 并不是 Fiber 之外的一层平行系统。相反,它就是建立在 Fiber 节点、更新队列和调度流程上的。
这也是为什么 Hooks 规则会那么严格: React 需要依赖稳定的调用顺序,把“这次执行读到的是第几个 Hook”跟 Fiber 上保存的链表状态一一对应起来。
结合一个真实例子,走一遍源码链路
前面这些概念如果只停留在定义层面,读起来还是容易飘。更好的办法,是抓一个非常普通的业务场景,看看它在 Fiber 里到底怎么跑。
我们先看一个最常见的函数组件:
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(c => c + 1)}>
{count}
</button>
);
}
用户点了一下按钮,页面上的 0 变成 1。从业务层看,这只是一次普通更新;但在 React 内部,这次更新通常会经过这样一条链路:
graph TD
A["点击按钮"] --> B["dispatchSetState"]
B --> C["requestUpdateLane"]
C --> D["enqueueConcurrentHookUpdate"]
D --> E["scheduleUpdateOnFiber"]
E --> F["renderRootSync / renderRootConcurrent"]
F --> G["beginWork"]
G --> H["updateFunctionComponent"]
H --> I["renderWithHooks"]
I --> J["reconcileChildren"]
J --> K["completeWork"]
K --> L["commitRoot"]
L --> M["DOM 文本从 0 变成 1"]
下面按这条链路往下拆。
1. 点击之后,先不是“立刻重新渲染”,而是先创建 update
在当前 React 主线源码里,useState 返回的 dispatch 最终会落到 ReactFiberHooks.js 里的 dispatchSetState。
这里最核心的几步是:
- 先通过
requestUpdateLane(fiber)为这次更新分配 lane - 再把这次更新包装成一个 update 对象
- 然后通过
enqueueConcurrentHookUpdate把它挂到对应 Hook 的更新队列里 - 如果成功拿到了 root,再调用
scheduleUpdateOnFiber(root, fiber, lane)
这一步很关键,因为 React 还没有开始真正执行组件。它只是先回答两个问题:
- 这次更新属于什么优先级
- 这次更新应该回到哪棵 Fiber 树上去处理
如果你去看 dispatchSetState 那段源码,会看到它干的事情和我们平时对“setState 就是重新渲染”的直觉并不完全一样。更准确地说,它是“先登记一笔更新,再把对应 root 标记为需要工作”。
2. React 会沿着 Fiber 树往上,把工作一路标到 root
进入 scheduleUpdateOnFiber 之后,React 会把这次更新带着的 lane 往上冒泡。
可以把它想成这样:
- 当前
CounterFiber 自己有新工作了 - 它的父节点需要知道“我的子树里有活要干”
- 再往上的父节点也要知道
- 最后根节点会知道: 这棵树上有一个对应 lane 的更新等待处理
这也是为什么 Fiber 节点上除了 lanes,还会有 childLanes。前者更像“我自己身上的工作”,后者更像“我下面这整片子树里还有没有工作”。
等根节点被标脏之后,React 就会确保这个 root 被调度起来。具体是走同步还是并发工作循环,要看当前更新的优先级、执行环境以及 root 的模式。
3. 真正进入 render 后,函数组件会在 renderWithHooks 里重新执行
当 work loop 真正开始跑时,React 会从 root 开始处理 Fiber。走到 Counter 这个函数组件时,主线通常会来到:
beginWorkupdateFunctionComponentrenderWithHooks
这一段可以理解成“重新执行函数组件,但执行时带着一整套 React 的运行时上下文”。
renderWithHooks 里会发生几件关键的事:
- 取到当前 Fiber 上已经保存的 Hook 链表
- 按调用顺序依次读取
useState、useEffect等 Hook - 把更新队列里的 update 应用到旧状态上,算出新的
count - 最终重新执行组件函数,拿到新的 JSX 结果
这也是为什么 Hooks 的调用顺序不能乱。因为 React 并不是通过变量名识别“这是哪个 state”,而是通过“这个组件本次执行到第几个 Hook”去对齐旧 Hook 链表。
对上面这个例子来说,setCount(c => c + 1) 对应的 update 被消费后,组件重新执行,返回的按钮文本就从 0 变成了 1。
4. JSX 算出来之后,React 还要决定哪些 Fiber 能复用
组件函数重新执行完,不代表工作就结束了。React 还要继续处理新旧子节点的关系。
在函数组件场景下,你可以把这一步理解成:
renderWithHooks返回新的 childrenreconcileChildren开始对比新旧子树- 能复用的 Fiber 尽量复用
- 需要新增、删除、移动的地方打上对应标记
对 Counter 这个例子来说,树形结构本身几乎没变:
Counter这个函数组件 Fiber 还在button对应的 HostComponent Fiber 还在- 按钮里的文本节点对应的 HostText Fiber 也还在
真正变化的,是 HostText 对应的内容从 0 变成了 1。所以这次更新更多是在“复用现有 Fiber,然后给需要提交的节点打 Update 标记”,而不是创建一棵完全不同的新树。
5. completeWork 负责把提交所需的信息收回来
等子节点都处理完后,React 会在回溯阶段进入 completeWork。
这一步可以理解成“收尾并归并副作用信息”:
- 对宿主节点来说,判断需不需要创建实例、更新属性、拼接子节点
- 对文本节点来说,判断文本内容有没有变化
- 把当前节点和子树上的 flags 归并起来,方便 commit 阶段快速处理
在我们的例子里,文本从 0 变成 1,这类变化最终会被记录成 commit 阶段可执行的宿主更新。
6. 最后进入 commitRoot,页面才真的变
只有当整棵 workInProgress 树准备完成后,React 才会进入 commitRoot。
这时前面 render 阶段累积下来的结果,才会真正落到 DOM 上:
- mutation 阶段更新文本节点
- layout 阶段执行
useLayoutEffect - passive effects 刷新阶段再处理
useEffect
所以从源码视角看,“点击按钮后文字变了”并不是一个单独动作,而是一串步骤的最终结果:
- 先登记更新
- 再调度 root
- 再跑 render,算出新树
- 最后统一 commit
再看一个列表重排例子: key 为什么能决定状态命运
再看一个更能体现 Fiber diff 思路的例子:
function List({ items }) {
return (
<ul>
{items.map(item => (
<Item key={item.id} label={item.label} />
))}
</ul>
);
}
假设旧列表是:
[A, B, C]
新列表变成:
[B, A, C]
如果 key 分别就是 A、B、C,那么在 ReactChildFiber.js 的那条 diff 路径里,React 关心的不是“长得像不像”,而是“还是不是同一个节点”。
可以把处理过程理解成这张表:
| 新位置 | 节点 | 旧位置 | 结果 |
|---|---|---|---|
| 0 | B | 1 | 可以复用,先保留 |
| 1 | A | 0 | 可以复用,但需要移动 |
| 2 | C | 2 | 可以复用,保持不变 |
这里一个很关键的变量就是 lastPlacedIndex。
它可以粗暴理解成一句话: “到目前为止,已经确认不用动的旧节点,最靠后的那个位置在哪。”
顺着上面的例子看:
- 先处理
B时,它在旧列表里的位置是 1,大于等于当前lastPlacedIndex,所以它可以先留着 - 然后处理
A时,它在旧列表里的位置是 0,小于当前的lastPlacedIndex - 这说明
A虽然还是同一个节点,但它已经跑到一个“更靠前的旧位置”去了,于是 React 会给它打上Placement相关标记
这就是“复用”和“移动”可以同时成立的原因:
- Fiber 还是原来那个 Fiber
- 组件状态也可以保住
- 但 DOM 位置需要在 commit 阶段调整
这也是为什么稳定 key 这么重要。它直接决定了 React 是把一个节点当成:
- 同一个人,只是换了位置
- 还是一个旧节点消失了、一个新节点刚出现
如果这里不用稳定 key,而是直接用索引,当列表头部插入一个新项时,React 很可能会把后面一串节点都误认成“还是原来的位置上的人”。结果通常就是:
- 组件状态错位
- 输入框内容串行
- 动画和焦点表现异常
所以从源码角度说,key 从来不是“为了消除控制台 warning 才勉强补上的属性”,而是 Fiber 复用策略里非常核心的一部分。
如果你准备自己读源码,建议从哪几处进
Fiber 源码不算适合从头平铺着看。更高效的方式,是先抓主干文件,再顺着调用关系往下走。
我更推荐这样的入口顺序:
ReactFiber.jsReactFiberWorkLoop.jsReactFiberBeginWork.jsReactChildFiber.jsReactFiberCompleteWork.jsReactFiberCommitWork.js
可以这样理解它们的职责:
ReactFiber.js: Fiber 节点长什么样,current和workInProgress怎么关联ReactFiberWorkLoop.js: 整个渲染和提交流程怎么被驱动起来ReactFiberBeginWork.js: 某个 Fiber 开始处理时,如何决定是否继续往下算ReactChildFiber.js: 子节点如何复用、插入、删除,key到底怎么参与 diffReactFiberCompleteWork.js: 往回收时需要补哪些宿主层信息ReactFiberCommitWork.js: 真正提交副作用时做了哪些事情
如果你第一次读 React reconciler,我建议别一开始就追所有分支。先盯住“函数组件更新一次”或者“列表节点重排一次”这类具体场景,顺着调用链走,理解会快很多。
几个常见误区
Fiber 不是 Virtual DOM 的同义词
Virtual DOM 更像“用内存对象描述界面”的抽象;Fiber 是 React 用来执行 reconciliation 和 scheduling 的内部结构。两者相关,但不是一回事。
Fiber 不等于“React 会自动更快”
Fiber 更像是让 React “更会安排工作”。如果一个组件树本身就有很多无意义重渲染,Fiber 也不会凭空把这些浪费变没。
并发渲染不等于 Commit 也能被切一半
可中断的是 Render,不是已经开始的 DOM 提交。
老文章里的字段名,不一定还对应今天的源码
如果你看到 expirationTime、pendingWorkPriority、effectTag 这类说法,不一定全错,但很可能对应的是较旧版本实现。理解 Fiber 最好抓住稳定概念,比如:
- 可中断的 render
current/workInProgressalternate- lanes
- render / commit 分离
写在最后
我觉得理解 Fiber,最重要的不是背字段名,而是把心智模型转过来:
React 不再只是“收到更新,然后一口气重新跑完组件树”。从 Fiber 开始,它更像一个会调度的运行时系统。它知道什么事情应该立刻响应,什么事情可以延后,什么事情做了一半可以先停,什么旧结果已经过期可以直接丢掉。
这也是为什么 Fiber 之后,React 才真正有了并发特性、生效更自然的过渡更新,以及更细粒度的界面响应能力。
如果只用一句话收尾,我会这么概括:
Fiber 的价值,不是把 React 变成“更快的递归”,而是把 React 变成“会安排工作的渲染系统”。

