前言
本文的React代码版本为18.2.0
可调试的代码仓库为:GitHub - yyyao-hh/react-debug at master-pure
从React16开始,引入了Fiber架构。它彻底重构了React的调和机制,将不可中断的递归遍历,革新为可暂停、可恢复的链表遍历。这一根本性变革为React带来了时间切片、并发模式等能力,使其从“同步渲染”迈入了“异步渲染”的新纪元。本文将揭示这场革命的内核原理。
为什么使用 Fiber 架构?
React 核心思想
作为一个构建用户界面的JavaScript库,React的核心思想可以概括为:
内存中维护一颗虚拟DOM树,数据变化时自动更新虚拟DOM,然后通过Diff算法对比新旧虚拟DOM树,找出变化的部分并批量更新到真实DOM 。在React16之前,这个过程被分为两个阶段:调和阶段(Reconciler)和渲染阶段(Renderer )。
旧架构的不足
React16之前,在调和阶段(Reconciler)使用递归方式同步遍历虚拟DOM树,对比每个组件以确定需要更新的部分。这个过程React团队称为Stack Reconciler,因为它依赖于JavaScript内置的调用栈。这种递归模型虽然直观,但有一个致命缺陷:一旦开始就无法中断。如果组件树层级很深,递归调用会长时间占用JavaScript主线程,导致用户交互、动画等任务无法及时响应,造成页面卡顿。
浏览器渲染机制
要理解为什么长时间占用JavaScript线程会导致卡顿,我们需要了解浏览器的渲染机制。页面是一帧一帧绘制出来的,一般浏览器的刷新频率为60hz,即每秒绘制的60帧(FPS),这意味着每一帧只有约16.7ms的时间来执行所有任务。
在一帧之内,浏览器需要完成多个步骤:处理用户交互、JavaScript解析执行、requestAnimationFrame调用、布局、绘制等。如果JavaScript执行时间过长,超过了16ms,就会延迟后续的布局和绘制,导致帧率下降,用户自然会感觉到卡顿。
Fiber 的解决方案
为了解决Stack Reconciler的瓶颈,React团队从16 年开始重构协调算法,最终在React16中引入了Fiber Reconciler架构。
Fiber架构是用于实现虚拟DOM和组件协调的新的架构。核心理念是将渲染任务拆分为多个小的工作单元,而不是一次性同步处理整个组件树。旨在优化渲染过程、实现异步渲染,并提高应用的性能和用户体验。
Fiber架构通过实现以下目标来解决性能问题:
- 暂停工作,稍后再回来:可以中断渲染过程处理高优先级任务
- 为不同类型的工作分配优先级:确保用户交互和动画优先执行
- 复用以前完成的工作:提高渲染效率
- 如果不再需要,则中止工作:避免不必要的渲染
这种增量渲染的方式使得React能够在时间分片中处理更新,充分利用浏览器的空闲期执行任务,从而避免阻塞关键的用户交互。
React官方以漫画的方式形象的展示了两种架构运行区别。来源:Fiber Reconciler
Fiber 架构深度剖析
什么是 Fiber
Fiber可以从两个互补的角度来理解:执行单元和数据结构。
- 作为一个执行单元,
Fiber代表一个可以拆分的工作块。React将整个视图的更新过程分解为多个小的Fiber任务,每个任务通常只处理一个组件。与一次性完成整个更新不同,Fiber使React能够将工作分成小块,在浏览器空闲时执行它们。 - 作为一种数据结构,
Fiber是一个JavaScript对象,它由对应的React Element生成,包含了组件的详细信息及其输入和输出。从代码层面看,每个Fiber节点对应一个React元素,存储了组件的类型、状态、副作用等信息。
下面是Fiber节点的核心属性结构:
/* src/react/packages/react-reconciler/src/ReactFiber.old.js */
export type Fiber = {|
/* ************ 实例的静态属性 ************ */
tag: WorkTag, // 节点类型
key: null | string, // 用于协调算法的唯一标识
elementType: any, // 原始元素类型 (函数/类本身)
type: any, // 解析后的元素类型 (可能被babel处理过)
stateNode: any, // 关联的真实实例 (DOM节点/类组件实例)
/* ************ Fiber 链表结构 ************ */
return: Fiber | null, // 指向父节点
child: Fiber | null, // 指向第一个子节点
sibling: Fiber | null, // 指向兄弟节点
index: number, // 在子节点中的索引
ref:
| null
| (((handle: mixed) => void) & {_stringRef: ?string, ...})
| RefObject,
/* ************ 属性 & 状态 ************ */
pendingProps: any,
memoizedProps: any,
updateQueue: mixed,
memoizedState: any,
dependencies: Dependencies | null,
mode: TypeOfMode,
/* ************ 副作用与更新标记 ************ */
flags: Flags,
subtreeFlags: Flags,
deletions: Array<Fiber> | null,
nextEffect: Fiber | null,
firstEffect: Fiber | null,
lastEffect: Fiber | null,
/* ************ 调度优先级 ************ */
lanes: Lanes,
childLanes: Lanes,
/* ************ 双缓冲机制 ************ */
alternate: Fiber | null,
|};
协调机制
Fiber架构最核心的改变之一是用链表遍历替代了递归遍历。传统的递归遍历依赖于JavaScript的调用栈,而Fiber实现了自己的堆栈帧管理,通过child、sibling和return三个指针构建了一个树形链表结构。
return: Fiber | null, // 指向父节点
child: Fiber | null, // 指向第一个子节点
sibling: Fiber | null, // 指向兄弟节点
index: number, // 在兄弟节点中的位。
如果当前的App组件如下:
function App() {
return (
<div>
Study
<span>React</span>
</div>
);
}
则对应的Fiber结构如下图:每个节点只保存它的第一个子节点,其他子节点通过sibling指针单向存储,所有子节点都会通过return指针指向父节点。
这种链表结构使得React可以手动控制遍历过程,而不依赖于JavaScript的调用栈。遍历算法基于深度优先原则,但不再是递归实现。具体遍历相关的内容,将在下一节详细展开。
双缓存技术
Fiber架构采用双缓存技术,在内存中维护着两棵Fiber树:current tree和workInProgress tree。
current tree:当前屏幕上显示内容对应的Fiber树workInProgress tree:正在内存中构建的新的Fiber树
而应用的根节点(FiberRootNode)会通过current指针完成current tree的切换。
当开始一次更新时,React会从current树的根节点开始,为每个节点创建一个alternate(替代)节点,这些alternate节点就构成了workInProgress树。如果节点没有更新,React会复用previous fiber(前一个fiber)来最小化工作量。
初始化阶段 mount
- 容器挂载阶段
上节讲过,首先会通过ReactDOM.createRoot创建FiberRootNode和RootFiberNode。
FiberRootNode:整个应用的根容器RootFiberNode:Fiber树的根节点(根组件<App/>的父节点)
由于是初始化渲染,页面中没有任何DOM,所以RootFiber没有任何子节点
Render阶段
会根据组件的React Element在内存中构建wip tree。构建时会尝试复用current tree中的Fiber节点(具体的复用过程,会在diff算法章节详细展开)。
初始化渲染时,
wip tree中只有RootFiberNode存在对应的alternate节点。
Commit阶段
此时页面DOM更新为右侧tree对应的数据。current指针指向wip tree,使其变为current tree。
更新阶段 update
- 触发更新:
Render阶段
构建一棵新的wip tree。
- 触发更新:
Commit阶段
此时页面DOM更新为左侧tree对应的数据。current指针指向wip tree,使其变为current tree。
这种双缓存机制有两个主要优点:
- 减少内存分配:通过节点复用,减少不必要的垃圾回收
- 无缝切换:当
workInProgress tree构建完成时,直接将它作为新的current tree,避免页面闪烁
后面再提到
workInProgress多简称为wip
渲染流程
Fiber架构将渲染过程分为两个截然不同的阶段:Render阶段和Commit阶段。
Render阶段是可中断的异步过程,负责处理组件的渲染和Diff计算。这个阶段React执行以下操作:
- 更新状态和属性
- 调用生命周期方法(如
getDerivedStateFromProps) - 执行渲染函数获取子元素
- 对比新旧子元素(
Diff算法) - 标记需要更新的副作用(
effect)
由于Render阶段可能被高优先级任务中断,React使用副作用列表(effect list)来跟踪哪些节点需要更新。这是一个线性链表,包含了所有有副作用的Fiber节点,大大提高了commit阶段的效率。
Commit阶段是同步不可中断的,负责将Render阶段计算的结果应用到DOM上。这个阶段主要包括:
- 执行
DOM的增删改操作 - 更新
refs - 调用生命周期方法(如
componentDidMount、componentDidUpdate) - 执行
useEffect、useLayoutEffect的清理和触发函数
将这两个阶段分离确保了用户界面的稳定性:Render阶段可以多次中断和重试,但Commit阶段总是快速且同步的,确保DOM更新的一致性和原子性。
此处略做总结,实现原理都将在后续的文章中详细展开
总结
Fiber架构是React的一次重大革新,它通过重新实现协调算法,解决了大型应用场景下的性能瓶颈和用户体验问题。Fiber的核心价值可以总结为:
- 可中断的异步渲染:将渲染工作拆分为小任务,避免长时间阻塞主线程
- 基于优先级的调度:确保高优先级任务(如用户交互)快速响应
- 更流畅的用户体验:通过时间分片技术减少页面卡顿
下一章我们将了解Fiber树的构建过程:Render阶段