摘要
本文讲述 React Fiber 架构的应用目的、核心思想,阐述实现该架构需解决的任务碎片化、时间分片、在浏览器空闲时执行这三个问题,介绍 Fiber 对象、双缓存 Fiber 树、Scheduler 调度、Reconciler 改造,还提及 Fiber 可能存在的问题,最后总结 Fiber 架构的多方面意义以及其优秀设计方案的应用价值。
一、React Fiber 架构的应用目的与核心思想
React Fiber 架构的应用目的是实现增量渲染,而增量渲染是实现任务可中断、可恢复,赋予任务不同优先级以达成更顺滑用户体验的手段。其核心思想为可中断、可恢复与优先级。
(一)实现 Fiber 架构需解决的问题
要实现 Fiber 架构,必须解决三个问题:任务碎片化、时间分片、在浏览器空闲的时候执行。
二、Fiber 对象
(一)引入新数据结构的原因
React 15 架构使用树形结构串联虚拟 DOM 树并递归遍历,数据量大时,递归在内存和时间方面有弊端,且无法分解工作为增量单元,也不能暂停和恢复特定组件工作。虽然用循环遍历可满足中断要求,但树形结构下若没有辅助数据结构,现场保护会很复杂。
(二)Fiber 对象的结构与作用
React 官方把虚拟 DOM 树拍扁成链表形式,用 Fiber 对象作为链表节点单位。Fiber 对象是 Javascript 对象,包含 return、child 和 sibling 三个指针,连接父子兄弟节点构成单链表 fiber 树,将递归遍历改为循环遍历实现深度优先遍历。一个 Fiber 节点对应一个元素节点,它除了这三个指针还记录其他必要信息,如 DOM 节点基本信息和任务调度信息。
// packages/react-reconciler/src/ReactInternalTypes.js
export type Fiber = {|
// 作为静态数据结构,存储节点 dom 相关信息
tag: WorkTag, // 组件的类型,取决于 react 的元素类型
key: null | string,
elementType: any, // 元素类型
type: any, // 定义与此 fiber 关联的功能或类。对于组件,它指向构造函数;对于 DOM 元素,它指定 HTML tag
stateNode: any, // 真实 dom 节点
// fiber 链表树相关, 主要
return: Fiber | null, // 指向他在 Fiber 节点树中的`parent`,用来在处理完这个节点之后向上返回
child: Fiber | null, // 指向自己的第一个子节点
sibling: Fiber | null, // 指向自己的兄弟节点,兄弟节点的 return 指向同一个父节点
index: number, // 在父 fiber 下面的子 fiber 中的下标
ref:
| null
| (((handle: mixed) => void) & {_stringRef:?string,...})
| RefObject,
// 工作单元,用于计算 state 和 props 渲染
pendingProps: any, // 本次渲染需要使用的 props
memoizedProps: any, // 上次渲染使用的 props
updateQueue: mixed, // 用于状态更新、回调函数、DOM 更新的队列
memoizedState: any, // 上次渲染后的 state 状态
dependencies: Dependencies | null, // contexts、events 等依赖
mode: TypeOfMode,
// 副作用相关
flags: Flags, // 记录更新时当前 fiber 的副作用(删除、更新、替换等)状态
subtreeFlags: Flags, // 当前子树的副作用状态
deletions: Array<Fiber> | null, // 要删除的子 fiber
nextEffect: Fiber | null, // 下一个有副作用的 fiber
firstEffect: Fiber | null, // 指向第一个有副作用的 fiber
lastEffect: Fiber | null, // 指向最后一个有副作用的 fiber
// 优先级相关
lanes: Lanes,
childLanes: Lanes,
alternate: Fiber | null, // 指向 workInProgress fiber 树中对应的节点
actualDuration?: number,
actualStartTime?: number,
selfBaseDuration?: number,
treeBaseDuration?: number,
_debugID?: number,
_debugSource?: Source | null,
_debugOwner?: Fiber | null,
_debugIsCurrentlyTiming?: boolean,
_debugNeedsRemount?: boolean,
_debugHookTypes?: Array<HookType> | null,
|};
Fiber Node 不仅是数据节点结构,还是 React 的任务拆分单位,每个任务单元负责一个节点处理。
三、双缓存 Fiber 树
(一)双缓存技术原理
双缓存技术用于图像处理时可避免闪屏,其原理是在内存中构建当前帧,构建完直接替换上一帧。
(二)在 React 中的应用
React 中最多同时存在两棵 Fiber 树,第一次渲染后有 current Fiber 树反映当前屏幕内容,更新时在内存构建 workInProgress Fiber 树反映未来状态,构建完成后用它替换 current Fiber 树。workInProgress Fiber 树可看作工作快照,是不可见的,很多属性可复用 current Fiber 树,二者通过 alternate 属性建立关联。
function createWorkInProgress(current,...) {
let workInProgress = current.alternate;
if (workInProgress === null) {
workInProgress = createFiber(...);
}
...
workInProgress.alternate = current;
current.alternate = workInProgress;
...
return workInProgress;
}
四、Scheduler 调度
(一)requestIdleCallback
1. 基本语法与原理
基本语法为var handle = window.requestIdleCallback(callback[, options])
,其 callback 会接收 deadline 对象,通过它获取浏览器空闲时间和回调是否超时状态,可合理安排帧内任务,空闲就执行,时间不足就再次请求。也可传入 timeout 配置超时时间,但尽量少用,因为会有性能损失和丢帧风险。
type Deadline = {
timeRemaining: () => number // 当前剩余的可用时间。即该帧剩余时间。
didTimeout: boolean // 是否超时。
}
function work(deadline:Deadline) { // deadline 上面有一个 timeRemaining()方法,能够获取当前浏览器的剩余空闲时间,单位 ms;有一个属性 didTimeout,表示是否超时
console.log(`当前帧剩余时间: ${deadline.timeRemaining()}`);
if (deadline.timeRemaining() > 1 || deadline.didTimeout) { // didTimeout 为 true 表示是因为超时而被触发
// 走到这里,说明时间有余,我们就可以在这里写自己的代码逻辑
}
// 走到这里,说明时间不够了,就让出控制权给主线程,下次空闲时继续调用
requestIdleCallback(work);
}
requestIdleCallback(work, { timeout: 1000 }); // 这边可以传一个回调函数(必传)和参数(目前就只有超时这一个参数)
2. 存在的问题
requestIdleCallback 是实验性 API,浏览器支援度有限,语法和行为可能改变,且触发频率不稳定,切换 tab 时可能降低触发机会。
(二)requestAnimationFrame
它用于帧动画,回调在每一帧确认执行,属于高优先级任务,与屏幕刷新频率同步。
(三)Scheduler 任务调度器
1. 多个任务的管理
Scheduler 可宏观管理多个任务,微观节制单个任务执行。它引入任务优先级和时间片概念。任务有开始时间和到期时间,Scheduler 维护 6 种优先级,不同优先级对应不同 timeout 数值。任务进来时,根据开始时间和当前时间比较判断是否过期,分别推入 timerQueue(未过期队列)和 taskQueue(已过期队列),同队列任务根据不同标准排序,taskQueue 中的任务会被循环执行,timerQueue 中的任务会被检测是否过期,过期则移入 taskQueue。
2. 单个任务的执行控制
单个任务执行控制涉及任务中断和恢复,需要调度者和执行者配合。在浏览器每一帧时间里,JS 线程默认时间切片是 5ms,循环处理 taskQueue 中的任务时,每次 while 循环退出就是一个时间切片用尽,执行 task.callback 前要进行超时检测。
五、Reconciler 改造
Scheduler 调度器可将任务切片提交到 Reconciler 调和器。React 15 架构中 Reconciler 与 Renderer 交替进行,强行中断更新会导致页面更新不完全,而 Fiber 架构下 Reconciler 执行过程分为 render / reconciliation phase 与 commit phase。render 阶段工作可中断,能暂停、中止和重新开始,commit 阶段工作不可中断,因为会更新真实 DOM。在 render 阶段,React 可根据时间片处理一个或多个 fiber 节点,能保存执行到一半的工作,重新获取时间片后可继续工作。遇到高优先级任务时,render 阶段工作会被中止,重新执行更新任务前的生命周期函数,所以 render 阶段的生命周期函数尽量不要做有副作用操作。
六、Fiber 可能存在的问题
(一)生命周期函数多次执行
Fiber 更新分两个阶段,reconciliation 阶段可打断,可能导致 commit 前的生命周期函数多次执行,官方已标记部分传统类组件生命周期函数为 unsafe,推荐使用新的生命周期函数。
(二)饥饿问题
高优先级任务一直插入可能使低优先级任务无法执行,官方提出尽量复用已完成操作来缓解。
七、总结
Fiber 从不同角度有不同意义,从架构看是核心算法重写,从编码看是数据结构,从工作流看是工作单元。React Fiber 架构中的优秀设计方案可应用到平时的架构设计中。