摘要
本文讲述 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 架构中的优秀设计方案可应用到平时的架构设计中。