react-Fiber

loading 2023年01月07日 154次浏览

Fiber是对react核心算法的重构,在react16中被引入

1. 为什么需要Fiber?

我们知道,在浏览器中,页面是一帧一帧绘制出来的,渲染的帧率与设备的刷新率保持一致。一般情况下,设备的屏幕刷新率为1s 60次,当每秒内绘制的帧数(FPS)超过60时,页面渲染是流畅的;而当FPS小于60时,会出现一定程度的卡顿现象。

js引擎和页面渲染引擎是在同一个渲染线程之内,两者是互斥关系。如果在某个阶段执行任务特别长,那么就会阻塞页面的渲染,从而出现卡顿现象。

在 react16 引入 Fiber 架构之前,react 会采用深度优先搜索的方法递归对比虚拟DOM树,找出需要变动的节点,然后同步更新它们,这个过程 react 称为reconcilation(协调)。 在reconcilation期间,react 会一直占用浏览器资源,会导致用户触发的事件得不到响应。传统的遍历方法:

这种遍历是递归调用,执行栈会越来越深,而且不能中断,中断后就不能恢复了。递归如果非常深,就会十分卡顿。如果递归花了100ms,则这100ms浏览器是无法响应的,代码执行时间越长卡顿越明显。传统的方法存在不能中断和执行栈太深的问题。

为了解决这个问题,React16将递归的无法中断的更新重构为异步的可中断更新,由于曾经用于递归的虚拟DOM数据结构已经无法满足需要。于是,全新的Fiber架构应运而生。

通过Fiber架构,让reconcilation过程变得可被中断,适时地让出CPU执行权,可以让浏览器及时地响应用户的交互。

2. 什么是Fiber?

React内部实现的一套状态更新机制。支持任务不同优先级,可中断与恢复,并且恢复后可以复用之前的中间状态。

作为架构来说,之前React15的Reconciler采用递归的方式执行,数据保存在递归调用栈中,所以被称为stack Reconciler。React16的Reconciler基于Fiber节点实现,被称为Fiber Reconciler。

也可以理解为是一个执行单元

作为动态的工作单元来说,每个Fiber节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新...)。

也可以理解为是一种数据结构

作为静态的数据结构来说,每个Fiber节点对应一个React element,保存了该组件的类型(函数组件/类组件/原生组件...)、对应的DOM节点等信息。

2.1 一个执行单元

Fiber 可以理解为一个执行单元,每次执行完一个执行单元,react 就会检查现在还剩多少时间,如果没有时间则将控制权让出去。

  • react向浏览器请求调度,浏览器在一帧中如果还有空闲时间,则去判断是否存在待执行任务
    • 如果存在就执行对应的任务,执行完毕后继续判断是否还有空闲时间,是否还存在待执行任务,如此往复
    • 如果不存在就将控制权交给浏览器

Fiber可以被理解为划分一个个更小的执行单元,它是把一个大任务拆分为了很多个小块任务,一个小块任务的执行必须是一次完成的,不能出现暂停,但是一个小块任务执行完后可以移交控制权给浏览器去响应用户,从而不用像之前一样要等那个大任务一直执行完成再去响应用户。

2.2 一种数据结构

Fiber 还可以理解为是一种数据结构,React Fiber 就是采用链表实现的每个 Virtual DOM 都可以表示为一个 fiber

如下图所示,每个节点都是一个fiber。一个 fiber包括了child(第一个子节点)、sibling(兄弟节点)、return(父节点)等属性,React Fiber机制的实现,就是依赖于以下的数据结构。

需要注意,Fiber 是 React 进行重构的核心算法,fiber是指数据结构中的每一个节点,如下图所示的A1、B1都是一个 fiber。

3. Fiber中使用到的两个api

3.1 requestAnimationFrame

前期用到了这个api,由于浏览器兼容性和不稳定的原因,后期改用自己实现的优先级调度机制Scheduler。

Fiber中使用到了这个api,它是浏览器提供的绘制动画的 api 。它要求浏览器在下次重绘之前(即下一帧)调用指定的回调函数更新动画

举个例子,让浏览器在每一帧中,将页面 div 元素的宽变长1px,直到宽度达到100px停止,这时就可以采用requestAnimationFrame来实现这个功能。

<body>
  <div id="div" class="progress-bar "></div>
  <button id="start">开始动画</button>
</body>

<script>
  let btn = document.getElementById('start')
  let div = document.getElementById('div')
  let start = 0
  let allInterval = []

  const progress = () => {
    div.style.width = div.offsetWidth + 1 + 'px'
    div.innerHTML = (div.offsetWidth) + '%'
    if (div.offsetWidth < 100) {
      let current = Date.now()
      allInterval.push(current - start)
      start = current
      requestAnimationFrame(progress)
    } else {
      console.log(allInterval) // 打印requestAnimationFrame的全部时间间隔
    }
  }

  btn.addEventListener('click', () => {
    div.style.width = 0
    let currrent = Date.now()
    start = currrent
    requestAnimationFrame(progress)
    console.log(allInterval)
  })

</script>

3.2 requestIdleCallback

前期用到了这个api,由于浏览器兼容性和不稳定的原因,后期改用自己实现的优先级调度机制Scheduler

requestIdleCallback也是实现react Fiber用到的api。

我们希望能够快速响应用户,让用户觉得够快,不能阻塞用户的交互,requestIdleCallback能使开发者在主事件循环上执行后台和低优先级的工作,而不影响延迟关键事件,如动画和输入响应。正常帧任务完成后没超过16ms,说明有多余的空闲时间,此时就会执行requestIdleCallback里注册的任务。

具体的执行流程如下,开发者采用requestIdleCallback方法注册对应的任务,告诉浏览器我的这个任务优先级不高,如果每一帧内存在空闲时间,就可以执行注册的这个任务
另外,开发者是可以传入timeout参数去定义超时时间的,如果到了超时时间了,浏览器必须立即执行,使用方法如下:window.requestIdleCallback(callback, { timeout: 1000 })。
浏览器执行完这个方法后,如果没有剩余时间了,或者已经没有下一个可执行的任务了React应该归还控制权,并同样使用requestIdleCallback去申请下一个时间片。

4. Fiber链表结构

Fiber Tree实际上是个单链表树结构,节点之间通过child、sibling、return三个属性连接。

而Fiber tree中的每个节点被称为fiber(就换了个小写字母...),fiber节点结构如下:

{
    type: any, // 对于类组件,它指向构造函数;对于DOM元素,它指定HTML tag
    key: null | string, // 唯一标识符
    stateNode: any, // 保存对组件的类实例,DOM节点或与fiber节点关联的其他React元素类型的引用
    child: Fiber | null, // 大儿子
    sibling: Fiber | null, // 下一个兄弟
    return: Fiber | null, // 父节点
    tag: WorkTag, // 定义fiber操作的类型, 详见https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactWorkTags.js
    nextEffect: Fiber | null, // 指向下一个节点的指针
    updateQueue: mixed, // 用于状态更新,回调函数,DOM更新的队列
    memoizedState: any, // 用于创建输出的fiber状态
    pendingProps: any, // 已从React元素中的新数据更新,并且需要应用于子组件或DOM元素的props
    memoizedProps: any, // 在前一次渲染期间用于创建输出的props
    // ……     
}

5. Fiber执行原理

从根节点开始渲染和调度的过程可以分为两个阶段:render和commit

  • render:这个阶段是可中断的,会找出所有节点的变更
  • commit:这个阶段是不可中断的,会执行所有的变更

5.1 render/reconciliation阶段

此阶段会找出所有节点的变更,如节点新增、删除、属性变更等,这些变更 react 统称为副作用(effect)。

此阶段会构建一棵Fiber tree(Work In Progress),以虚拟dom节点为维度对任务进行拆分,即一个虚拟dom节点对应一个任务,最后产出的结果是effect list,从中可以知道哪些节点更新、哪些节点增加、哪些节点删除了

workInProgress fiber的创建可以复用current Fiber树对应的节点数据,决定怎么复用的过程就是diff算法

5.1.1 遍历

React Fiber首先将虚拟DOM树转化为Fiber树(Work In Progress),因此每个节点都有child,sibling,return属性。

遍历虚拟DOM采用DFS:

的过程称为beginwork
的过程称为completework

5.1.2 收集effect list

知道了遍历方法之后,接下来需要做的工作就是在遍历过程中,收集所有节点的变更产出effect list,其中包含了变更了的节点。

每个节点更新结束时向上归并自己的effect list,最后根节点的effect list就收集了所有变更信息。

收集effect list的具体步骤为:

  1. 如果当前节点需要更新,则打tag更新当前节点状态
  2. 为每个子节点创建fiber。如果没有产生child fiber,则结束该节点,把effect list归并到return,把此节点的sibling节点作为下一个遍历节点;否则把child节点作为下一个遍历节点
  3. 如果有剩余时间,则开始下一个节点,否则等下一次主线程空闲再开始下一个节点
  4. 如果没有下一个节点了,进入pendingCommit状态,此时effect list收集完毕,结束。

5.2 commit阶段

commit阶段需要将上阶段计算出来的需要处理的副作用一次性执行,此阶段不能暂停,否则会出现UI更新不连续的现象。此阶段需要根据effect list,将所有更新都commit到DOM树上

随后,fiber根节点会将本来指向current tree的指针转为指向wip tree,使其成为新的current tree。

6. 两棵fiber tree

React Fiber 是 React 的核心算法的重新实现,它的主要特点是渐进式渲染。在 React Fiber 中,会有两棵 Fiber 树,即当前树(current tree)工作树(work-in-progress tree)

6.1 what

当所有ReactElement比较完后,会生成一棵fiberNode树(WIP),一共会存在两棵fiberNode树:

workInProgress:触发更新后,正在reconciler中计算的fiberNode树
current:与视图中真实UI对应的fiberNode树

6.2 why

在 React 的 Fiber 架构中,使用两棵 Fiber 树(即当前树和工作树)的主要目的是实现渐进式渲染。这种设计允许 React 在内存中构建新的工作树,然后与当前在屏幕上渲染的树进行比较,找出需要更新的部分。这种方式可以避免在更新过程中阻塞主线程,从而提高性能

如果只有一棵 Fiber 树,并且让 React Element 的修改直接应用在上面,那么在构建新的树的过程中,可能会阻塞主线程,导致用户界面无法及时响应用户的操作。

总的来说,工作树起到了一个中间人或缓冲的作用,它允许 React 在内存中进行大量的计算,而不会阻塞主线程

6.3 how

  1. 根据新的reactElement来修改WIP树

挂载div

// React Element <div></div>
jsx("div")

// 对应fiberNode
null

// 生成子fiberNode

// 对应标记
Placement

将div更新为p

// React Element <p></p>
jsx("p")

// 对应fiberNode
FiberNode {type: 'div'}

// 生成子fiberNode

// 对应标记
Deletion Placement
  1. 比较
    在 React 的渲染过程中,实际上只有一次比较,那就是将 Work-In-Progress(WIP)树与 Current 树进行比较。这个比较过程被称为 “Reconciliation” 或 “Diffing”,其目的是找出两棵树之间的差异。

    当 WIP 树构建完成后,React 会将其与 Current 树进行比较,找出需要更新的部分。

  2. 应用
    最后,React 会将更新完毕的wip tree应用到 DOM 上,这样用户就能看到新的 UI。然后再改变指针,使得wip tree成为新的current tree,循环往复。