React 中的 Fiber 架构是什么?

330 阅读6分钟

前言

fiber 很老的知识点了,今天我们再来学习一遍,好的设计永远不会过时,时看时新

  • fiber 是 react 16.18.0 版本引入
  • React 通过 Fiber 架构,让自己的 Reconcilation 过程变成可被中断。 '适时'地让出 CPU 执行权,除了可以让浏览器及时地响应用户的交互

fiber

why fiber

为什么出现 fiber,这是个好问题?

  • 历史问题,16 版本之前

    1. 调和阶段(Reconciler):react 自顶向下递归,遍历先数据生成的 Virtual DOM,然后通过 diff 算法,找到需要变更的元素,放到更新队列
    2. 渲染阶段(Renderer): 遍历更新队列,渲染

上述过程中呢,第一阶段,一旦任务开始,就无法中止,只能等待它结束,js 一直占据着主线程,直到虚拟 DOM 树构建完成,然后开始渲染;这样就会导致一些用户交互、动画等任务无法得到立即处理,卡顿,影响用户体验

  • 那需要如何去解决这些问题呢?

    1. 首先需要了解一个知识点,浏览器是一帧一帧绘制出来的,当每秒绘制的帧数达到 60 的时候,页面才会是流畅,小于这个值,用户会感觉到卡顿(1s 60 帧,所以每一帧分到的时间是 1000/60 ≈ 16 ms,所以我们书写代码时力求不让一帧的工作量超过 16ms)
    2. 浏览器一帧内需要完成多少的工作呢:处理用户的交互;js 解析执行;开始帧(window resize、scroll、mediaquery changed、animation events);rAF(requestAnimationFrame);布局;绘制
    3. 2 中的六个步骤,任意一个时间占用过长,总时间超过 16 ,用户都会感知到卡顿(所以上述 Reconciler 过程中,如果 js 执行时间过长,又不能中断,本来应该渲染下一帧了,当时当前一帧还在执行 js ,用户交互得不到反馈,有卡顿感)

所以 react 就采用了合作式调度,将 react 的渲染更新分成多个任务,每次只做一部分任务,做完再看是否还有剩余时间,如果有继续做,如果没有,挂起当前任务,将控制权力交还到主线程,有空余时间再继续

  • 如果做到合作式调度呢? 合作调度就是用来分配任务的,当有更新的任务来到时候,不会立马去做 diff 操作,而是将当前需要的更新推进一个更新队列中,然后交给 Scheduler 处理,Scheduler 会根据当前主线程的情况去处理;为了实现这种效果,使用了 requestidlecallback API,对于不支持此 api 的浏览器,react 进行了自己的实现 requestidlecallback:在浏览器的两个执行帧之间,主线程通常会有一段空闲的时间(Idle Period),requestidlecallback 的作用就是在这个空闲的时间,调用回调(Idle Callback),执行我们想要执行的任务

fiber 需要做到什么

  • fiber 需要做到什么

    1. 暂停工作,并且可以恢复
    2. 为不同的工作分配优先权
    3. 重用已经完成的工作
    4. 如果不再需要,则中止

    为了完成上述工作,将任务分解为单元,fiber 为一个工作单元 fiber 为一种数据结构,它的特性为时间分片和暂停

  • fiber 实际怎么工作的

    1. 把接收到的 react element 转换为 fiber 节点,并且为其设置优先级,加入到更新队列,做一些初始数据的准备
    2. scheduleWork、requestWork、performWork,即安排工作、申请工作、正式工作;这部分即是 schedule 阶段
    3. 遍历所有 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 节点继续遍历下去

两个阶段

  1. reconciliation 协调阶段:可以认为是 diff 阶段,这个阶段会找出所有的节点变更,这个阶段可以被中断(和之前版本的区别) 以下生命周期钩子会在协调阶段被调用

    • constructor
    • componentWillMount 废弃
    • componentWillReceiveProps 废弃
    • static getDerivedStateFromProps
    • shouldComponentUpdate
    • componentWillUpdate 废弃
    • render
  2. 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)就好了

双缓冲的好处:

  1. 可以复用旧的节点对象
  2. 减少内存分配,GC 的时间开销
  3. 异常处理:运行中有错误,可以沿用旧的节点,不会影响整体

可以将 WIP 树想象成从旧树中 Fork 出来的功能分支,你在这新分支中添加或移除特性,即使是操作失误也不会影响旧的分支。当你这个分支经过了测试和完善,就可以合并到旧分支,将其替换掉,然后 commit,合理!!!

后记

本文更多的使用语言去描述了 fiber 架构是什么,希望看完这篇文章,大家心中都有一个对 fiber 的基本认知!!!