- 技术难点:同步递归渲染为什么非改不可
React 16 之前(以及 Vue 至今的大部分更新),渲染一棵组件树用的是朴素递归:render() 返回 vdom,协调器对比新旧 vdom 后同步改 DOM。在中小项目里它简单可靠,但有一个致命结构缺陷:
- 递归一旦开始就无法停下。React 的函数组件天然是树形递归,一个组件渲染完才能轮到下一个。整棵树的协调工作挤在一个调用栈里同步跑完,中途没有任何"检查点"。当一次更新涉及几千个组件时,主线程被独占几十甚至上百毫秒,期间用户的输入、滚动、动画全部排队等待。浏览器一帧只有约 16.6ms,超过这个预算就会掉帧、卡输入。
- 隐式调用栈没有"存档"能力。递归的状态全压在函数栈帧里,那是运行时私有数据,框架够不着。想"做一半暂停,让高优先级任务插队,再回来接着做",就必须能回答一个问题:下一次该从哪个组件继续。调用栈给不出这个答案,它只能一路弹完。
- 副作用不能中途发生。如果协调过程中边对比边改 DOM,一旦被中断,页面上就会残留半新半旧的状态,用户看到撕裂的界面。所以任何"可中断"方案的前提,是把副作用全部推迟到最后一口气做完。
这三点合起来指向一个结论:渲染必须从"一次函数调用"变成"可调度的工作队列"。而队列里的每一项得是一个能独立暂停、能被记住、能被丢弃重来的最小工作单元,这就是 Fiber。
- 完整解法:Fiber 架构四件套
2.1 把调用栈摊平成链表:FiberNode
Fiber 的本质,是把递归调用栈显式化为一个可持久化的数据结构。每个组件对应一个 Fiber 节点,节点之间不靠函数嵌套,而靠三个指针连接:
child:第一个子节点sibling:下一个兄弟节点return:父节点(指回去,遍历结束要能向上回溯)
一棵树因此变成了可随意进出、可存档恢复的链表结构。真实的 FiberNode 还挂着 type/key(对应 vdom 元素)、pendingProps/memoizedProps(新旧 props)、stateNode(组件实例或真实 DOM)、alternate(双缓存对端)、lanes(优先级位掩码)、flags(副作用标记)。
2.2 双缓存树:用户永远看不到"半成品"
React 同时维护两棵树:current(已经提交、正在屏幕上展示的树)和 workInProgress(在后台静默构建的新树),通过 alternate 互指。协调阶段只在 workInProgress 上施工,屏幕上的一切不受打扰;整棵树构建完毕才做一次指针切换,current 整体替换。这就是难点 3 的解法:要么不动,要么一次到位,中间态永远对用户不可见,丢弃半成品树也毫无心理负担。
2.3 render 阶段:DFS 工作循环
render 阶段从根出发,逐个处理工作单元,顺序是标准深度优先:
beginWork:处理当前 Fiber(按需创建 DOM、调和 children 生成子 Fiber)- 有
child就下钻;没有就completeWork收尾当前节点 - 向右走
sibling;兄弟走完沿return向上找下一个带兄弟的祖先
主循环每次只调用 performUnitOfWork 处理一个单元,处理完把"下一个单元"存进全局 nextUnitOfWork。这个变量就是整个架构的断点存档:无论何时被中断,下次回来从它接着走即可,不需要重建任何调用栈。
2.4 时间切片 + 优先级抢占:并发的地基
Scheduler 用宏任务(MessageChannel,不用 requestIdleCallback,兼容性差且有 50ms 调用上限问题)驱动工作循环,每个宏任务内设置一个 deadline(约 5ms 预算),workLoop 每处理完一个单元就检查是否超时:
<span>while</span> (nextUnitOfWork && !<span>shouldYield</span>()) {
nextUnitOfWork = <span>performUnitOfWork</span>(nextUnitOfWork);
}
超时就主动让出主线程,浏览器得以渲染一帧、响应输入,下个宏任务再回来继续。同时每个更新按 lane 位掩码标记优先级:离散事件(输入、点击)对应最高优先级,startTransition 包住的更新是低优先级。高优先级任务到达时,直接丢弃正在构建的低优先级 workInProgress 树,低优先级任务转入重试队列稍后再来。render 阶段是纯函数、无副作用,所以丢弃永远安全。被中断、被抢占、被重建,用户看到的始终是完整一致的旧树,直到新树 commit。
2.5 commit 阶段:不可中断的"最后一口气"
所有真实副作用(DOM 插入删除、useLayoutEffect、useEffect)都推迟到 commit 阶段,按 mutation → layout → passive 三段同步执行,绝不切片。因为 DOM 一旦开始改动,中断就会留下中间态,commit 必须一口气跑完。所以 Fiber 的全部灵活性都在 render 阶段,commit 阶段是"牺牲灵活性换原子性"。
2.6 可运行的最小实现
下面这段代码把上述骨架浓缩成约百行,Node 直接可跑。它演示低优先级大列表渲染过程中,高优先级输入事件到达时的完整行为:时间片让出 → 抢占 → 丢弃低优先级树 → 高优先级 commit → 低优先级重试。代码注释标出了与真实 React 的对应关系。
<span>// mini-fiber.mjs —— 最小可运行 Fiber 协调器(Node 直接跑)</span>
<span>// 运行: node mini-fiber.mjs</span>
<span>// ---------- 1. 超轻量 DOM 桩:只记录挂载顺序,便于观察 commit ----------</span>
<span>const</span> <span>createEl</span> = (<span>tag</span>) => ({ tag, <span>children</span>: [] });
<span>const</span> <span>createText</span> = (<span>text</span>) => ({ <span>tag</span>: <span>'#text'</span>, text });
<span>// ---------- 2. vdom 工厂(不依赖 JSX) ----------</span>
<span>const</span> <span>h</span> = (<span>type, props, ...children</span>) => ({
type,
<span>props</span>: props ?? {},
<span>// 字符串子节点统一规范化为 TEXT 节点,与 JSX 文本子节点行为一致</span>
<span>children</span>: children.<span>flat</span>().<span>map</span>(<span>(<span>c</span>) =></span>
<span>typeof</span> c === <span>'string'</span> || <span>typeof</span> c === <span>'number'</span>
? { <span>type</span>: <span>'TEXT'</span>, <span>props</span>: { <span>nodeValue</span>: <span>String</span>(c) }, <span>children</span>: [] }
: c,
),
});
<span>// ---------- 3. Fiber 与全局状态 ----------</span>
<span>let</span> wipRoot = <span>null</span>; <span>// 正在构建的 workInProgress 树根(双缓存的后台树)</span>
<span>let</span> nextUnit = <span>null</span>; <span>// 下一个工作单元 = 中断恢复点(对应 nextUnitOfWork)</span>
<span>let</span> epoch = <span>0</span>; <span>// 渲染代数:抢占重建时自增,旧单元据此作废</span>
<span>function</span> <span>createFiber</span>(<span>vnode, parent</span>) {
<span>return</span> { vnode, parent, <span>dom</span>: <span>null</span>, <span>child</span>: <span>null</span>, <span>sibling</span>: <span>null</span> };
}
<span>function</span> <span>createDom</span>(<span>fiber</span>) {
<span>const</span> { type, props } = fiber.<span>vnode</span>;
<span>if</span> (type === <span>'TEXT'</span>) <span>return</span> (fiber.<span>dom</span> = <span>createText</span>(props.<span>nodeValue</span>));
fiber.<span>dom</span> = <span>createEl</span>(type);
<span>for</span> (<span>const</span> k <span>in</span> props) <span>if</span> (k !== <span>'nodeValue'</span>) fiber.<span>dom</span>[k] = props[k];
}
<span>// beginWork:建 DOM + 把 children 串成 child/sibling 链表</span>
<span>function</span> <span>beginWork</span>(<span>fiber</span>) {
<span>if</span> (fiber.<span>vnode</span>.<span>type</span> !== <span>'ROOT'</span>) <span>createDom</span>(fiber);
<span>if</span> (fiber.<span>vnode</span>.<span>type</span> === <span>'TEXT'</span>) <span>return</span>;
<span>let</span> prev = <span>null</span>;
<span>for</span> (<span>const</span> c <span>of</span> fiber.<span>vnode</span>.<span>children</span>) {
<span>const</span> f = <span>createFiber</span>(c, fiber);
<span>if</span> (!prev) fiber.<span>child</span> = f;
<span>else</span> prev.<span>sibling</span> = f;
prev = f;
}
}
<span>// 深度优先取下一个单元:有 child 下钻;否则向右找 sibling;再向上找带 sibling 的祖先</span>
<span>function</span> <span>nextWorkUnit</span>(<span>fiber</span>) {
<span>if</span> (fiber.<span>child</span>) <span>return</span> fiber.<span>child</span>;
<span>let</span> n = fiber;
<span>while</span> (n) {
<span>if</span> (n.<span>sibling</span>) <span>return</span> n.<span>sibling</span>;
n = n.<span>parent</span>;
}
<span>return</span> <span>null</span>;
}
<span>const</span> <span>slow</span> = (<span>ms</span>) => { <span>const</span> t = <span>Date</span>.<span>now</span>() + ms; <span>while</span> (<span>Date</span>.<span>now</span>() < t) {} };
<span>function</span> <span>perform</span>(<span>fiber</span>) {
<span>beginWork</span>(fiber);
<span>slow</span>(<span>0.5</span>); <span>// 模拟复杂组件计算量,让时间片真实耗尽</span>
<span>return</span> <span>nextWorkUnit</span>(fiber);
}
<span>// commit:一次性把整棵新树挂到容器。对应真实 React 的 commit 阶段:不可中断</span>
<span>function</span> <span>commit</span>(<span>fiber, container</span>) {
<span>if</span> (!fiber) <span>return</span>;
<span>if</span> (fiber.<span>dom</span> && fiber.<span>parent</span>?.<span>dom</span>) fiber.<span>parent</span>.<span>dom</span>.<span>children</span>.<span>push</span>(fiber.<span>dom</span>);
<span>commit</span>(fiber.<span>child</span>, container);
<span>commit</span>(fiber.<span>sibling</span>, container);
}
<span>// ---------- 4. 调度器:时间切片 + 优先级抢占 ----------</span>
<span>const</span> <span>SLICE_MS</span> = <span>3</span>; <span>// 一个时间片的预算,耗尽就让出主线程</span>
<span>let</span> renderLabel = <span>''</span>;
<span>let</span> pendingHigh = <span>null</span>; <span>// 抢占注入点(模拟高优先级事件到达)</span>
<span>let</span> retryQueue = []; <span>// 被抢占的低优先级任务,稍后重试(对应 transition 语义)</span>
<span>let</span> sliceCounter = <span>0</span>; <span>// 日志节流</span>
<span>function</span> <span>startRender</span>(<span>vnode, container, label, fromScrap</span>) {
epoch++;
<span>const</span> root = <span>createFiber</span>({ <span>type</span>: <span>'ROOT'</span>, <span>props</span>: {}, <span>children</span>: [vnode] }, <span>null</span>);
root.<span>dom</span> = container;
wipRoot = root;
nextUnit = root;
renderLabel = label;
<span>console</span>.<span>log</span>(<span>`[<span>${label}</span>] <span>${fromScrap ? <span>'重建(epoch='</span> + epoch + <span>')'</span> : <span>'开始渲染(epoch='</span> + epoch + <span>')'</span>}</span> 队列中有 <span>${countNodes(vnode)}</span> 个节点`</span>);
<span>workLoop</span>();
}
<span>function</span> <span>workLoop</span>(<span></span>) {
<span>const</span> deadline = <span>Date</span>.<span>now</span>() + <span>SLICE_MS</span>;
<span>let</span> done = <span>0</span>;
<span>while</span> (nextUnit && <span>Date</span>.<span>now</span>() < deadline) { <span>// shouldYield() 的雏形</span>
nextUnit = <span>perform</span>(nextUnit);
done++;
}
<span>if</span> (!nextUnit) {
<span>commit</span>(wipRoot.<span>child</span>); <span>// 树建完,一次性提交,切换 current 树</span>
<span>console</span>.<span>log</span>(<span>`[<span>${renderLabel}</span>] commit 完成,容器内节点: <span>${container.children.map((c) => c.tag).join(<span>' -> '</span>)}</span>`</span>);
wipRoot = <span>null</span>;
<span>if</span> (retryQueue.<span>length</span>) { <span>// 高优先级提交后,让被抢占的低优先级任务重试</span>
<span>const</span> [v, c, l] = retryQueue.<span>shift</span>();
<span>startRender</span>(v, c, l, <span>true</span>);
} <span>else</span> <span>if</span> (pendingHigh) {
<span>const</span> [v, c, l] = pendingHigh;
pendingHigh = <span>null</span>;
<span>startRender</span>(v, c, l, <span>false</span>);
}
<span>return</span>;
}
<span>if</span> (++sliceCounter % <span>5</span> === <span>0</span>) {
<span>console</span>.<span>log</span>(<span>`[<span>${renderLabel}</span>] 时间片耗尽(已处理 <span>${done}</span> 单元),让出主线程,等待下个宏任务`</span>);
}
<span>if</span> (pendingHigh) {
<span>// 抢占:丢弃当前 workInProgress 树(render 无副作用,丢弃安全),任务转入重试队列</span>
<span>console</span>.<span>log</span>(<span>` >> 高优先级到达!丢弃低优先级 workInProgress 树,任务转入重试队列`</span>);
retryQueue.<span>push</span>([wipRoot.<span>vnode</span>.<span>children</span>[<span>0</span>], wipRoot.<span>dom</span>, renderLabel]);
<span>const</span> [v, c, l] = pendingHigh;
pendingHigh = <span>null</span>;
<span>startRender</span>(v, c, l, <span>false</span>); <span>// 高优先级立即重建并优先 commit</span>
} <span>else</span> {
<span>setTimeout</span>(workLoop, <span>0</span>); <span>// 让出主线程,下个宏任务回来继续</span>
}
}
<span>const</span> <span>countNodes</span> = (<span>v</span>) =>
v.<span>type</span> === <span>'TEXT'</span> ? <span>1</span> : <span>1</span> + v.<span>children</span>.<span>reduce</span>(<span>(<span>s, c</span>) =></span> s + <span>countNodes</span>(c), <span>0</span>);
<span>// ---------- 5. 场景演示 ----------</span>
<span>// 低优先级:渲染 300 项大列表(901 个 fiber 单元,必然跨多个时间片)</span>
<span>const</span> bigList = <span>h</span>(<span>'ul'</span>, <span>null</span>,
...<span>Array</span>.<span>from</span>({ <span>length</span>: <span>300</span> }, <span>(<span>_, i</span>) =></span> <span>h</span>(<span>'li'</span>, { <span>'data-i'</span>: i }, <span>h</span>(<span>'span'</span>, <span>null</span>, <span>'item '</span> + i))));
<span>// 高优先级:一个输入框小组件(离散事件对应最高优先级 lane)</span>
<span>const</span> inputView = <span>h</span>(<span>'div'</span>, <span>null</span>, <span>h</span>(<span>'input'</span>, { <span>type</span>: <span>'text'</span> }), <span>h</span>(<span>'span'</span>, <span>null</span>, <span>'已输入: '</span>));
<span>const</span> container = <span>createEl</span>(<span>'root'</span>);
<span>console</span>.<span>log</span>(<span>'=== 场景:低优先级大列表渲染中,用户输入事件(高优先级)到达 ===\n'</span>);
<span>setTimeout</span>(<span>() =></span> { pendingHigh = [inputView, container, <span>'高:输入框'</span>]; }, <span>60</span>);
<span>startRender</span>(bigList, container, <span>'低:大列表'</span>, <span>false</span>);
实际运行的关键输出(已节流):
<span>[低:大列表]</span> 开始渲染(<span>epoch</span>=<span>1</span>) 队列中有 <span>901</span> 个节点
<span>[低:大列表]</span> 时间片耗尽(已处理 2 单元),让出主线程,等待下个宏任务
...(反复让出)...
>> 高优先级到达!丢弃低优先级 workInProgress 树,任务转入重试队列
<span>[高:输入框]</span> 开始渲染(<span>epoch</span>=<span>2</span>) 队列中有 <span>4</span> 个节点
<span>[高:输入框]</span> commit 完成,容器内节点: div
<span>[低:大列表]</span> 重建(<span>epoch</span>=<span>3</span>) 队列中有 <span>901</span> 个节点
<span>[低:大列表]</span> commit 完成,容器内节点: div -> ul
注意两个细节:低优先级任务从未"做一半就提交",高优先级 commit 后容器里只有 div(输入框),随后低优先级重建(epoch 3)并整体提交,容器从 div 干净地变成 div -> ul。中间的每次让出和抢占,用户都感知不到任何撕裂。
- 应用场景
- 大列表输入卡顿:表格几千行、搜索框每敲一个字触发一次全表重渲染,同步渲染必然丢帧。用 Fiber 的优先级体系,输入更新走最高 lane 直接 commit,列表重渲染降级为可抢占的普通更新,甚至包进
useDeferredValue/startTransition让它在空闲时间片里慢慢做完。这是并发特性对业务最直接的收益。 - Suspense 与数据加载:组件抛出一个 Promise,Fiber 把该分支标记为挂起并中断构建,Promise resolve 后重新调度该分支。Suspense 能"等一等再渲染"的机制与抢占共用同一套可中断渲染地基,React 19 的
use()让这个模型从lazy扩展到任意异步资源。 - 保持交互响应性的代价模型:写 React 时要区分"急事"和"不急的事"。状态更新默认可能被中断,副作用必须放进 effect(commit 阶段),render 里永远不做有副作用的事。这个心智模型一旦建立,
useTransition的返回值(isPending)和useDeferredValue就都是顺理成章的工具。 - 与 Vue 3 的对比:Vue 用细粒度响应式把更新范围缩到最小,再靠微任务批量同步提交,大部分更新根本不需要中断;React 粒度粗(整树协调),但用可中断换取"永远不让用户等"。两者一个省事、一个让路,是两种工程哲学,不是简单的谁快谁慢。
- 总结
Fiber 架构只回答了一个问题:怎么让渲染可以随时停下、记住进度、被人插队、还能安全重来。答案是放弃函数调用栈的便利,把工作显式地做成数据结构:链表化的 fiber 树提供断点,双缓存保证原子可见,时间切片把大任务拆进帧间隙,lane 优先级让插队有秩序,commit 阶段用"不可中断"换"永不撕裂"。React 18 的并发、Suspense、过渡更新,React 19 的 Actions,全部是这套地基上的应用层。看任何 React 源码或报错栈时记住一句话:看到 beginWork 是它在往下钻,看到 completeUnitOfWork 是它在回溯,看到 "render was interrupted" 是它被更高优先级的人插了队。这套"递归变循环、调用栈变显式数据"的思路,离开 React 也通用,协程、redux-saga、甚至操作系统的进程调度,都是同一个故事。
把 Fiber 讲成"递归变循环、调用栈变显式数据",比逐行读源码更易建立整体心智模型。适合想搞懂并发、Suspense 底层,或需要排查渲染卡顿的前端工程师。