前言
fiber 很老的知识点了,今天我们再来学习一遍,好的设计永远不会过时,时看时新
- fiber 是 react 16.18.0 版本引入
- React 通过 Fiber 架构,让自己的 Reconcilation 过程变成可被中断。 '适时'地让出 CPU 执行权,除了可以让浏览器及时地响应用户的交互
fiber
why fiber
为什么出现 fiber,这是个好问题?
-
历史问题,16 版本之前
- 调和阶段(Reconciler):react 自顶向下递归,遍历先数据生成的 Virtual DOM,然后通过 diff 算法,找到需要变更的元素,放到更新队列
- 渲染阶段(Renderer): 遍历更新队列,渲染
上述过程中呢,第一阶段,一旦任务开始,就无法中止,只能等待它结束,js 一直占据着主线程,直到虚拟 DOM 树构建完成,然后开始渲染;这样就会导致一些用户交互、动画等任务无法得到立即处理,卡顿,影响用户体验
-
那需要如何去解决这些问题呢?
- 首先需要了解一个知识点,浏览器是一帧一帧绘制出来的,当每秒绘制的帧数达到 60 的时候,页面才会是流畅,小于这个值,用户会感觉到卡顿(1s 60 帧,所以每一帧分到的时间是 1000/60 ≈ 16 ms,所以我们书写代码时力求不让一帧的工作量超过 16ms)
- 浏览器一帧内需要完成多少的工作呢:处理用户的交互;js 解析执行;开始帧(window resize、scroll、mediaquery changed、animation events);rAF(requestAnimationFrame);布局;绘制
- 2 中的六个步骤,任意一个时间占用过长,总时间超过 16 ,用户都会感知到卡顿(所以上述 Reconciler 过程中,如果 js 执行时间过长,又不能中断,本来应该渲染下一帧了,当时当前一帧还在执行 js ,用户交互得不到反馈,有卡顿感)
所以 react 就采用了合作式调度,将 react 的渲染更新分成多个任务,每次只做一部分任务,做完再看是否还有剩余时间,如果有继续做,如果没有,挂起当前任务,将控制权力交还到主线程,有空余时间再继续
- 如果做到合作式调度呢? 合作调度就是用来分配任务的,当有更新的任务来到时候,不会立马去做 diff 操作,而是将当前需要的更新推进一个更新队列中,然后交给 Scheduler 处理,Scheduler 会根据当前主线程的情况去处理;为了实现这种效果,使用了 requestidlecallback API,对于不支持此 api 的浏览器,react 进行了自己的实现 requestidlecallback:在浏览器的两个执行帧之间,主线程通常会有一段空闲的时间(Idle Period),requestidlecallback 的作用就是在这个空闲的时间,调用回调(Idle Callback),执行我们想要执行的任务
fiber 需要做到什么
-
fiber 需要做到什么
- 暂停工作,并且可以恢复
- 为不同的工作分配优先权
- 重用已经完成的工作
- 如果不再需要,则中止
为了完成上述工作,将任务分解为单元,fiber 为一个工作单元 fiber 为一种数据结构,它的特性为时间分片和暂停
-
fiber 实际怎么工作的
- 把接收到的 react element 转换为 fiber 节点,并且为其设置优先级,加入到更新队列,做一些初始数据的准备
- scheduleWork、requestWork、performWork,即安排工作、申请工作、正式工作;这部分即是 schedule 阶段
- 遍历所有 fiber 节点,通过 diff 算法计算到所有更新队列,产出 EffectList 给到 commit 阶段使用
what is fiber
链表结构
- react 目前使用的是一个
链表
结构(链表是啥,大家应该很熟悉了,不熟悉的同学看下链表讲解),每一个 virtual dom 节点内部使用 fiber 表示
export type Fiber = {
// Fiber 类型信息
type: any,
// ...
// ⚛️ 链表结构
// 指向父节点,或者render该节点的组件
return: Fiber | null,
// 指向第一个子节点
child: Fiber | null,
// 指向下一个兄弟节点
sibling: Fiber | null,
};
- fiber 保存了节点处理的上下文信息,可以使用迭代的方式来处理这些节点
- 操作 fiber 时候,按照深度遍历的顺序返回下一个 Fiber
- 因为使用了链表结构,即使处理流程被中断了,我们随时可以从上次未处理完的 fiber 节点继续遍历下去
两个阶段
-
reconciliation 协调阶段:可以认为是 diff 阶段,这个阶段会找出所有的节点变更,这个阶段可以被中断(和之前版本的区别) 以下生命周期钩子会在协调阶段被调用
- constructor
- componentWillMount 废弃
- componentWillReceiveProps 废弃
- static getDerivedStateFromProps
- shouldComponentUpdate
- componentWillUpdate 废弃
- render
-
commit 提交阶段:commit 应用这些 DOM 变更,这个阶段不可以中断 以下生命周期钩子在提交阶段被执行
- getSnapshotBeforeUpdate() 严格来说,这个是在进入 commit 阶段前调用
- componentDidMount
- componentDidUpdate
- componentWillUnmount
由于 reconciliation 阶段是可中断的,一旦中断之后恢复的时候又会重新执行,所以很可能 reconciliation 阶段的生命周期方法会被多次调用,所以在 reconciliation 阶段的生命周期的方法是不稳定的;因此建议 协调阶段的生命周期钩子不要包含副作用. 索性 React 就废弃了这部分可能包含副作用的生命周期方法,例如 componentWillMount、componentWillUpdate
双缓冲
- React 在 render 第一次渲染时,会创建一颗 Element 树,可以称之为 Virtual DOM Tree,由于要记录上下文信息,加入了 Fiber,每一个 Element 会对应一个 Fiber Node,将 Fiber Node 链接起来的结构成为 Fiber Tree。它反映了用于渲染 UI 的应用程序的状态。这棵树通常被称为 current 树(当前树,记录当前页面的状态)
- WIP 树(WorkInProgress Tree)就是一个缓冲,它在 Reconciliation 完毕后一次性提交给浏览器进行渲染。它可以减少内存分配和垃圾回收,WIP 的节点不完全是新的,比如某颗子树不需要变动,React 会克隆复用旧树中的子树
- WorkInProgress Tree 构造完毕,得到的就是新的 Fiber Tree,然后喜新厌旧(把 current 指针指向 WorkInProgress Tree,丢掉旧的 Fiber Tree)就好了
双缓冲的好处:
- 可以复用旧的节点对象
- 减少内存分配,GC 的时间开销
- 异常处理:运行中有错误,可以沿用旧的节点,不会影响整体
可以将 WIP 树想象成从旧树中 Fork 出来的功能分支,你在这新分支中添加或移除特性,即使是操作失误也不会影响旧的分支。当你这个分支经过了测试和完善,就可以合并到旧分支,将其替换掉,然后 commit,合理!!!
后记
本文更多的使用语言去描述了 fiber 架构是什么,希望看完这篇文章,大家心中都有一个对 fiber 的基本认知!!!