引言
在 React 16 以前,React的更新方式是这样的:一次性、不可中断的深度递归更新。这种更新方式:
- 采用调用递归树来遍历组件树
- 从根节点开始递归render
- 中间过程不能暂停、不能打断
- 一旦开始,就必须一次性执行到结束
这种更新方式被称作:Stack Reconciler(调用栈调和器)。 其优点是显而易见的:实现简单,逻辑清晰,理论上来说性能不会太差,小更新速度非常快,不可打断的一次性能保证渲染的一致性,包括UI的一致性和生命周期顺序的固定可靠。
但是,也正是因为这种不可打断的一次性,带来了非常致命的问题:大组件树会卡死主线程。
React 的更新逻辑是运行在 JS 主线程中的,但是 JS 主线程除了负责 React 的更新,还需要去负责:
- UI渲染
- 用户输入事件
- 动画
- 网络回调
- 等等
如果我们需要渲染一个巨型的组件树, React 就会一次性递归计算组件树的所有节点,占用了 JS 主线程,并且中途无法暂停,从而导致 JS 堵塞,页面卡死,用户的交互事件也没有反应------因为主线程都被哪去给 React 渲染组件树了。
不仅于此,Raact 阻塞进程还会导致浏览器无法响应更高优先级的任务,在 React 16 以前,React 不会让用户输入等更高优先级的任务去插队响应,JS 线程必须等待当前组件树 render 完成,才能去响应其他任务------给用户的体验就是:页面卡顿、掉帧、输出延迟。
此外,因为递归栈形式的遍历不能中断,因此也很难去保存当前执行的上下文,记录执行到哪一步从而恢复执行,这种方式完全无法实现渐进式渲染,包括分片等功能自然也无法实现。
总结:React 16 以前使用的Stack Reconciler,优点是实现简单、一次性渲染保证一致性;缺点是不能中断、不能分片、不能恢复,导致长任务会阻塞主线程,引发页面卡顿,因此被Fiber替代。
React 开发团队也正是为了实现 "可中断、可恢复"的渲染,从而引入了Fiber的概念。
Fiber 的定义
什么是 Fiber
一句话概括:Fiber 是 React 用来执行"可中断更新"的一种数据结构(FiberNode)+调度机制(Workloop)。
Fiber 既是一个轻量级数据结构(描述组件的工作单元),又是一个任务调度系统(让更新分片执行,不卡主线程)。
可以这么说,Fiber=数据结构+调度系统。
Fiber 的引入使得渲染过程可以中断,并且根据需要重新调度任务,这种可中断可分片的优化使得 React 可以更好利用 JS 主线程的空闲时间,优化性能,提升用户体验。
Fiber 是一种数据结构
为什么这么说呢?我们可以看看FiberNode的源码。
tsx
export class FiberNode {
constructor(tag, pendinfProps, 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;
// props
this.pendingProps = pendingProps;
this,memoizedProps = null;
// 状态
this.memoizedState = null;
this.updateQueue = null;
// Effect 相关
this.flags = NoFlags;
this.subtreeFlags = NoFlags;
this.deletions = null;
// 调度 lane
this.lanes = NoLanes;
this.childLanes = NoLanes;
// 双缓存树
this.alternate = null;
}
}
我们可以重点关注以下几个内容。
1. child / sibling / return
Fiber 是一个非常典型的链表结构+树结构的混合,而非一棵标准的多叉树。
tsx
child → 第一个子节点
sibling → 右兄弟节点
return → 父节点
2. alternate
Fiber 使用的是"双缓存"机制,alternate 是双缓存树的核心。
Fiber 会同时维护两棵 Fiber 树:
- 一棵是当前页面正在用的树(current)
- 另一棵是正在计算下一帧UI的树(workInProgress)
计算完成后,就用一次性"切换指针"的方式把新的树换成 current,渲染过程可以实现无闪烁、可中断、不卡顿。双缓存依赖 alternate 形成两棵树,alternate 相当于两棵树切换的桥梁,能让 React 同时维护 current 树和 workInProgress 树,并在它们之间随时切换,无需重新创建整棵树。
每个 Fiber 节点都有两个版本,alternate 字段去记录它们的映射关系:
tsx
currentFiber.alternate = workInProgressFiber
workInProgressFiber.alternate = currentFiber
这种桥梁关系可以使 React 快速从 current 得到对应的 WIP,计算下一帧UI树时也不需要 new 新的树,而是直接复用已有的节点,在更新的过程中也会保存上一次的状态。
这里延伸一下:alternate 为什么可以让 Fiber 重用已有的节点?这里可以直接参考源码:
tsx
if (current.alternate !== null) {
// reuse
workInProgress = current.alternate;
} else {
// create a new fiber
}
这意味着,在 current.alternate 存在时,Fiber 会直接复用已有的节点,而不是创建一个新的Tree Node,这将大大减少性能。
这也意味着,WIP 的构建是可以随时产生随时停止的,只需要根据 current 即可随时构造,那么,在 JS 线程中,一旦有更高优先级的任务发生,WIP 可以立即被丢弃,等待更高优先级的任务完成,然后再重新算,期间不会对 current 造成任何影响,用户只能看到 current 的内容,页面体验也不会受到影响。
在 WIP 构建完成后,UI 切换并不是重新创建,而是"调换指针"。
tsx
root.current = finishedWork;
Alternate 保证两棵树的节点是一一对应的,那么只需要切换指针,就能实现将 WIP 推到页面上。
用一句话总结:Alternate 是双缓存的核心,因为它让 React 能维持 current 和 workInProgress 两棵并行的 Fiber 树。所有的更新都在WIP上进行,最终通过 alternate 快速切换指针使页面更新。 这种方式带来了可中断、可恢复、可丢弃、低卡顿的并发渲染能力。
3. lans
React 17 内部引入 lanes 取代 expirationTime,但 lanes 的真实能力在 React 18 并发模式中才真正向开发者开放。
Lans 模型决定:
- 哪些任务可以中断
- 哪些任务可以合并
- 哪些任务需要优先执行
这是并发模式的基础。
Fiber 是一种调度系统
说 Fiber 是一种调度系统,本质上是抓住了 Fiber 的设计核心:Fiber 不是一个单纯的树结构,而是一个可以拆分任务、按优先级执行、暂停和恢复的工作调度引擎。上文提到的 lans 很好地揭示了这样的一种行为。
为什么说 Fiber 是一种调度系统
1. 可中断/可恢复
React 16 以前,渲染是"递归+同步"的渲染:一旦开始渲染,就无法停止。Fiber 支持将渲染工作拆分成小单元,渲染间隙可以将 JS 线程让给优先级更高的任务,等待任务完成后再回来继续渲染。
这种机制非常像操作系统到的 CPU 调度:Fiber 是任务线程/进程,React 根据需求去调度。
2. 优先级机制
操作系统的 CPU 调度会提到优先级这个概念,根据优先级安排任务调度顺序,保证高优先级的任务优先被执行。
Fiber 也可以实现类似的优先级调度机制,支持不同优先级的任务,实现原理就是上文提到的 Lans,通过优先级调度系统,React 可以"插队"执行高优先级任务,例如在渲染过程中来了一个用户交互任务,React 可以暂停渲染先去处理交互任务,完成后再返回继续执行渲染任务。
上下文的储存也会保证不会丢失以前的渲染结果。这个优先级机制是并发渲染的基础。
3. 时间分片
Fiber 可以通过"时间分片"的思想,将大的渲染任务分解为小的任务,每一帧都只完成部分任务。这样 JS 主线程不会长时间被渲染任务占据,浏览器调度 API 在此基础上可以决定什么时候继续渲染任务,这样可以保证用户交互、动画等高优先级的任务不会被低优先级的渲染任务卡住。
4. 调度器
React 有一个单独的调度层(Scheduler),与 Fiber 结合,从而实现决定何时执行任务。这个调度层支持优先级任务、超时、挂起、恢复等,是一个真正的任务调度系统。
5. 可撤销/可放弃任务
如果正在执行的渲染任务优先级低于高优先级的用户交互任务,则当前的渲染任务可以被暂停,断点执行时机可以被推后或者放弃。渲染进度会被保存,下次可以从中断处恢复,这样,React 的渲染就不是一次性的必须全部成功或者全部失败,而是有"弹性"的。
从调度系统角度看 Fiber 的优势
一句话总结:更好地实现可中断、可恢复、低卡顿的渲染与页面交互。
- 更好响应页面交互:高优先级的用户交互任务被优先执行,低优先级任务可以被打断,使用户的页面使用体验更流畅。
- 提升页面性能:渲染任务被分片分时渲染,减少了 React 长任务占用 JS 主线程的时间,浏览器可以及时进行重绘/回流。
- 支持并发特性:Fiber 是 React 并发模式的基础,调度系统为并发渲染提供了核心与基石。
- 错误恢复能力更强:Fiber 会记住渲染的上下文,渲染任务中断或放弃不会影响现有页面与整个渲染任务,可以根据上下文恢复渲染任务。
Fiber 如何实现渲染
这里我们要研究实现渲染的关键函数。
1. 初次渲染:创建双缓存树
初次渲染时没有 alternate,于是 React 会创建一棵新的 workInProgress Fiber 树。
这里可以去看一看 Fiber 的源码 ReactFiber.js。
tsx
function createWorkInProgress(current, pendingProps) {
let workInProgress = current.alternate;
if (workInProgress === null) {
// 初次渲染:current 没有 alternate,会去创建一个新的 Fiber
workInProgress = new FiberNode(current.tag, pendingProps, current.key);
workInProgress.stateNode = current.stateNode;
workInProgress.alternate = current;
current.alternate = workInProgress;
}
return workInProgress;
}
初次渲染之后,current 和 WIP 两棵树就建好了,彼此通过 alternate 连接,相互复用,减少了后续节点构建的压力(因为后续就复用了)。
2. 更新阶段:利用双缓存机制构建新的UI
更新流程发生在 ReactFiberWorkLoop.js 中。
核心步骤如下:
- 从 current 树中获取对应 Fiber;
- 通过 createWorkInProgress 创建或复用 WIP 树;
- 所有的计算、diff、effect 都在 WIP 树种进行;
- 完成后执行 commit 阶段;
- 用改变指针的方式交换两棵树的角色:WIP 变成新的 current。
① Render 阶段:构建 WIP 树
循环由 performUnitOfWork(WIP) 驱动实现。
tsx
function performUnitOfWork(unitOfWork) {
const current = unitOfWork.alternate;
let next = beginWork(current, unitOfWork);
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
completeUnitOfWork(unitOfWork);
}
return next;
}
渲染都在WIP上计算,如果节点有子节点,那就进入子节点继续处理,直到节点没有子节点了(叶子节点),则向上归并进入 complete 阶段。
② Commit 阶段:切换缓存树
commit 阶段有两个特点:
- 同步执行,不可中断
- 会真正地更新 DOM/UI
在 commitLayoutEffects 阶段,React 会在浏览器执行绘制(paint)之前执行所有 Layout 副作用,统一在同一帧内完成,以避免 layout thrashing(布局抖动)。
由 commitRoot 执行角色交换,交换方式是指针指向修改,commit 阶段又被拆成三个阶段。来自ReactFiberWorkLoop.js:
tsx
function commitRoot(root) {
const finishedWork = root.finishedWork;
root.finishedWork = null;
// 阶段1:mutation 前
commitBeforeMutationEffects(root, finishedWork);
// 阶段2:mutation
commitMutationEffects(root, finishedWork);
root.current = finishedWork ;
// 阶段3:layout 阶段
commitLayoutEffects(root, finishedWork);
}
阶段一:mutation 前
该阶段发生在更新 DOM 前,用于为 mutation 做准备。
tsx
function commitBeforeMutationEffects(root, firstChild) {
let nextEffect = firstChild;
while (nextEffect !== null) {
const flags = nextEffect.flags;
if (flags & Snapshot) {
commitBeforeMutationEffectOnFiber(nextEffect);
}
nextEffect = nextEffect.nextEffect;
}
}
在这个阶段,React 会遍历所有带 Snapshot 标记的 Fiber,Snapshot 标记对应 getSnapshotBeforeUpdate。
这么做的目的是让我们在 DOM 更新前拿到更新前的 DOM 的真实状态的"快照",如果在更新后再获取,就不完全准确了。
React 是一个增量更新的 Fiber 树,不是一个单纯的 DOM 树,可能会出现如下情况:
- 多个组件有 Snapshot 副作用
- 有些嵌套组件会产生 Snapshot
- 某些组件的子组件没有更新,但子组件仍然需要 Snapshot
因此 React 不能只处理当前节点,它需要确保:每一个 Fiber 节点,只要在本次更新中被标记为 Snapshot,都必须在 DOM 更新前执行快照。
在这个阶段,只处理 Snapshot,不需要考虑其它flags,因为其它 flags 不是必须在 DOM 更新前执行。比如:
- Placement(插入 DOM):mutation 阶段
- Update(更新属性):mutation 阶段
- Deletion(删除DOM):mutation 阶段
- useEffect:Layout 后异步执行
总结:如果被问到 commitBeforeMutationEffects 要遍历所有 Snapshot Fiber 的原因,可以这么回答:
因为 Snapshot 对应 getSnapshotBeforeUpdate,它的语义要求"读取旧 DOM 状态"。
所以必须在 mutation 阶段(DOM 更新)之前执行。
React 通过遍历 effectList 上所有带 Snapshot 标记的 Fiber,确保所有组件在 DOM 被修改之前完成快照读取。
这保证了组件在 update 阶段能获得精准的 DOM 变化前的状态,使滚动恢复、光标位置保存、布局测量等成为可能。
阶段二:mutation
这个阶段是真正更新 DOM 的阶段,所有的 DOM 变动都在这一步完成。
tsx
function commitMutationEffects(root, finishedWork) {
let nextEffect = finishedWork
while (nextEffect !== null) {
const flags = nextEffect.flags
// 删除节点
if (flags & Deletion) {
commitDeletion(root, nextEffect)
}
// 插入或移动节点
if (flags & Placement) {
commitPlacement(nextEffect)
}
// 更新属性或文本
if (flags & Update) {
commitWork(nextEffect)
}
nextEffect = nextEffect.nextEffect
}
}
React 用 flags 标记 effect,常见的与 mutation 相关的 flags 有:
- Placement:需要插入(mount)或移动节点
- Deletion:需要删除(unmount)节点
- Update:更新属性/文本
- Snapshot:在 before-mutation 阶段处理(已在前面完成)
- Ref:需要更新/清理 ref(通常在 layout 阶段也会处理)
- Passive:useEffect(但 cleanup 在 mutation 阶段需要先执行其 cleanup,再在 layout/after 写入)
处理顺序也有先后,遵循Deletion → Placement → Update 的顺序,这样可以保证 DOM 父节点的稳定性。
阶段三:layout 阶段
在 DOM 更新后执行:
- 类组件的
componentDidMount - 类组件的
componentDidUpdate - hook 的
useLayoutEffectcreate 部分 - 调用 ref 回调
tsx
function commitLayoutEffects(root, finishedWork) {
let nextEffect = finishedWork;
while (nextEffect !== null) {
if (nextEffect.flags & Update) {
commitLayoutEffectOnFiber(root, nextEffect);
}
nextEffect = nextEffect.nextEffect;
}
}
主要作用就是:执行所有需要"看到最新 DOM"的副作用,从而实现同步、强制、阻塞,保证顺序一致,且能访问最新 DOM。
主流程如下:
tsx
function commitLayoutEffects(finishedWork, root, committedLanes) {
// ① 清理上次的 passive destroy(useEffect cleanup)
flushSyncCallbacksOnlyInLegacyMode();
// ② 遍历 layoutEffects
commitLayoutEffectsOnFiber(root, finishedWork, committedLanes);
}
function commitLayoutEffectsOnFiber(root, finishedWork, lanes) {
let nextEffect = finishedWork.firstEffect;
while (nextEffect !== null) {
// 执行 layout 的 work
commitLayoutEffectOnFiber(
root,
nextEffect.alternate,
nextEffect,
lanes
);
nextEffect = nextEffect.nextEffect;
}
}
其中,我们重点关注一下commitLayoutEffectsOnFiber,这个阶段是根据 flags 分类处理副作用:
tsx
function commitLayoutEffectOnFiber(root, current, finishedWork, lanes) {
const flags = finishedWork.flags;
if (flags & Update) {
// 处理 class 组件的 didUpdate 生命周期
}
if (flags & Callback) {
// setState 的回调
}
if (flags & Ref) {
// 安装 ref
}
// 重点:处理 Layout effects
if (flags & LayoutMask) {
commitHookEffectListMount(HookLayout, finishedWork)
}
}
其中 LayoutMask 包括 useLayoutEffect 的销毁和创建。对于 useLayoutEffect,React会先执行 destory,再执行 create,这里的先后顺序永远不会被打破。
强调!useLayoutEffect 必须同步执行?
因为这一阶段的 DOM 已经更新,依然可以同步读取,但是还没有渲染在浏览器上,这时 useLayoutEffect 可以进行 DOM 的尺寸测量和位置计算、修改 DOM ,以及注入同步逻辑。 React 故意将 useLayoutEffect 安排在渲染浏览器之前,从而避免渲染在页面的 DOM 会出现闪烁,这是一个同步行为。
如果时会被问到这样的问题------为什么 useEffect 不在这个阶段执行?我们可以这么回答:
因为 useLayoutEffect 是同步 + 阻塞的,而 useEffect 是异步 + 不阻塞渲染的(调度到微任务/宏任务),所以 useEffect 不会在这个阶段的layout 执行,而是在下一轮事件循环执行。
以上阶段内容总结:
commitLayoutEffects 是 React commit 阶段的"布局阶段",在 DOM 已更新但浏览器尚未绘制时执行。
它同步执行 useLayoutEffect 的 destroy 与 create、类组件的 didMount/didUpdate、ref 赋值等所有需要访问最新 DOM 的副作用。
React 通过 effect list 遍历仅更新的 Fiber,保证副作用按严格顺序执行,并确保 DOM 状态一致性。
补充:为什么 Fiber 是"最小可工作单元"
因为 Fiber 以前是不可中断、不可暂停的"递归调用栈",Fiber 的出现把递归变成了"可迭代的单链表结构",每个 FiberNode 本质上是一个工作单元,这就是 Fiber "可中断"的根本原因。
React Fiber 的本质就是:把组件树从递归改写成可遍历的链表,让渲染变成可拆分的工作单元,从而可以暂停、恢复、丢弃、重做。
总结
React 16 引入 Fiber,本质上是用一套"可中断、可恢复"的调度系统来替代原先不可中断的 Stack Reconciler(递归调用栈)。Fiber 既是一种轻量级的数据结构(FiberNode),又是一套完整的渲染与调度机制(WorkLoop + Scheduler)。它通过:
- 双缓存机制
- 单链表化的树节点FiberNode
- 可分片与可中断的工作单元
- 基于优先级的调度模型
让 React 能够在渲染期间暂停、恢复、放弃任务,并在合适的时机继续执行,不去长时间的占据 JS 主线程,从而避免卡顿。
Fiber 的引入彻底改变了 React 的渲染模型,使得:
- 渲染变得可控、不再阻塞主线程
- 更高优先级的用户交互可以"插队"
- 更新可以分片完成(时间切片)
- DOM 更新被拆解成 before-mutation → mutation → layout 三阶段,更可预测、更加精细
可以说:Fiber 是 React 并发特性的基础,也是 React 性能优化的核心。
React 从同步递归 → 可调度的 Fiber 架构,是一次从"函数调用"到"任务调度"的根本性转变,为后续的 Concurrent Mode、Suspense 等特性奠定了全部基础。
以上为个人在学习过程中的一些理解和感悟,部分有参考,如有不足,欢迎指正。