你也可以理解的React Fiber,学废了吗

猿友 2020-09-24 14:57:09 浏览数 (4035)
反馈

文章来源于公众号:前端时光屋 作者:小豪

Fiber出现的背景?

在早期的 React 版本中,也就是 React16.8 版本之前。

大量的同步计算任务阻塞了浏览器的UI渲染。默认情况下,JS运算页面布局页面绘制渲染都是运行在浏览器的主线程当中,他们之间是互斥的关系。

如果 JS 运算持续占用主线程,页面就没法得到及时的更新,当我们调用setState更新页面的时候,React 会遍历应用的所有节点,与老的 dom 节点进行 diff 算法的对比,最小代价更新页面,即使这样,整个过程也是一气呵成,不能被打断的,如果页面元素很多,整个过程占用的时间就可能超过16毫秒,出现掉帧的现象。

针对这一现象,React 团队从框架层面对 web 页面的运行机制做了优化,此后,Fiber诞生了。

说到16ms,我们来看这样的一个概念

屏幕刷新率

  • 目前大多数设备的屏幕刷新率为60次/秒
  • 浏览器的渲染动画或页面的每一帧的速率也需要跟设备屏幕的刷新率保持一致。
  • 页面是一帧一帧绘制出来的,当每秒绘制的帧数(FPS)达到60时,页面是流畅的,小于这个值时,用户会感觉到卡顿。
  • 每个帧的预算时间是16.66毫秒(1秒/60)
  • 1s 60帧,所以我们书写代码时尽量不让一帧的工作量超过16ms

Fiber的诞生

解决主线程长时间被 JS 晕眩占用这一问题的基本思路,是将运算切割为多个步骤,分批完成。也就是说在完成一部分任务之后, 将控制权交回给浏览器,让浏览器有时间再进行页面的渲染。等浏览器忙完之后,再继续之前React未完成的任务。

旧版 React 通过递归的方式进行渲染,使用的是 JS 引擎自身的函数调用栈,它会一直执行到栈空为止

Fiber实现了自己的组件调用栈,它以链表的形式遍历组件树,可以灵活地暂停、继续和丢弃执行的任务。实现的方式是使用了 浏览器的requestIdleCallback这一 API。官方的解释是这样的:

window.requestIdleCallback()会在浏览器空闲时期依次调用函数,这就可以让开发者在主事件循环中执行后台优先级低的任务,而且不会像对动画和用户交互这些延迟触发产生关键的事件影响。函数一般会按先进先调用的顺序执行,除非函数在浏览器调用它之前就到了它的超时时间。

requestIdleCallback的核心用法

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

requestIdleCallback执行流程

requestIdleCallback执行流程

Fiber是什么

Fiber是一个执行单元

Fiber 是一个执行单元,每次执行完一个执行单元, React 就会检查现在还剩多少时间,如果没有时间就将控制权让出去

Fiber是一个执行单元

Fiber是一种数据结构

React 目前的做法是使用链表, 每个 VirtualDOM 节点内部表示为一个Fiber,它可以用一个 JS 对象来表示:

const fiber = {
  stateNode, // 节点实例
  child,     // 子节点
  sibling,   // 兄弟节点
  return,    // 父节点
}

Fiber是一种数据结构

Fiber之前的协调阶段

  • React 会递归比对VirtualDOM树,找出需要变动的节点,然后同步更新它们。这个过程 React 称为Reconcilation(协调)
  • 在Reconcilation期间,React 会一直占用着浏览器资源,一则会导致用户触发的事件得不到响应, 二则会导致掉帧,用户可能会感觉到卡顿

let root = {
  key: 'A1',
  children: [
    {
      key: 'B1',
      children: [
        {
          key: 'C1',
          children: []
        },
        {
          key: 'C2',
          children: []
        }
      ]
    },
    {
      key: 'B2',
      children: []
    }
  ]
}


function walk(element) {
  doWork(element);
  element.children.forEach(walk);
}


function doWork(element) {
  console.log(element.key);
}
walk(root);

在 Fiber 出现之前, React 会不断递归遍历虚拟 DOM 节点,占用着浏览器资源,积极地浪费性能,造成卡顿现象,且协调阶段是不能被打断的

React不断递归遍历虚拟DOM节点

Fiber 出现之后,通过某些 Fiber 调度策略合理分配 CPU 资源,让自己的协调阶段变成可被终端适时地让 CPU(浏览器)执行权,提高了性能优化。

协调阶段变成可被终端

Fiber执行阶段

每次渲染有两个阶段:Reconciliation(协调\render阶段)和Commit(提交阶段)

  • 协调阶段: 这个阶段可以被中断, 通过Dom-Diff算法找出所有节点变更,例如节点新增删除属性变更等等, 这些变更React 称之为副作用(Effect)
  • 提交阶段: 将上一个阶段计算出来的需要处理的副作用(Effects)一次性执行了。这个阶段必须同步执行,不能被打断

简单理解的话

  • 阶段1:生成Fiber树,得出需要更新节点信息。(可打断
  • 阶段2:将需要更新的节点一次性地批量更新。(不可打断

Fiber的协调阶段,可以被优先级较高的任务(如键盘输入)打断。

阶段1可被打断的特性,让优先级更高的任务先执行,从框架层面大大降低了页面掉帧的概率。

Fiber执行流程

render阶段

Fiber Reconciliation(协调) 在阶段一进行 Diff 计算的时候,会生成一棵 Fiber 树。这棵树是在 Virtual DOM 树的基础上增加额外的信息生成来的,它本质来说是一个链表。

render阶段

commit提交阶段

Fiber 树在首次渲染的时候会一次过生成。在后续需要 Diff 的时候,会根据已有树和最新 Virtual DOM 的信息,生成一棵新的树。这颗新树每生成一个新的节点,都会将控制权交回给主线程,去检查有没有优先级更高的任务需要执行。如果没有,则继续构建树的过程。

1.如果过程中有优先级更高的任务需要进行,则 Fiber Reconciler 会丢弃正在生成的树,在空闲的时候再重新执行一遍。

2.在构造 Fiber 树的过程中,Fiber Reconciler 会将需要更新的节点信息保存在Effect List当中,在阶段二执行的时候,会批量更新相应的节点。

细节拓展

render阶段是如何遍历,生成Fiber树的?

<div>

  

    

      

      

    

    

  
</div>

  • 从顶点开始遍历
  • 如果有第一个儿子,先遍历第一个儿子
  • 如果没有第一个儿子,标志着此节点遍历完成
  • 如果有弟弟遍历弟弟
  • 如果有没有下一个弟弟,返回父节点标识完成父节点遍历,如果有叔叔遍历叔叔
  • 没有父节点遍历结束

render节点遍历规则

commit阶段,是如何commit的?

类比 Git 分支功能,从旧树中 fork 出来一份,在新分支进行添加、删除和更新操作,经过测试后进行提交。

commit阶段,是如何commit的

以上就是W3Cschool编程狮关于你也可以理解的React Fiber,学废了吗的相关介绍了,希望对大家有所帮助。

0 人点赞