# React Fiber 原理介绍

# 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是一种数据结构

要理解下面这段过程,需要理解,下面几个参数的含义和目的

  • sleep 函数(duration):表示此函数的执行时间,duration表示持续时间

  • works:浏览器空闲时间 需要执行的任务列表(数组)

  • workLoop:循环执行的工具(函数)

  • requestIdleCallback(callback,options): 浏览器空闲时间需要执行的函数(浏览器自行调用)

  • callback(deadLine):空闲时需要执行的函数,该回调函数接收一个deadLine对象,作为参数,这个参数包含两个属性

    • timeRemaining():表示当前帧剩余的时间,也可理解为留给任务的时间还有多少
    • didTimeout:布尔值,表示任务是否超时,结合 timeRemaining 使用
  • options:目前options只有一个参数

    • timeout:表示超过这个时间后,如果任务还没执行,则强制执行,不必等待空闲
function sleep(duration){ // 1000
  let start = Date. now(); // 1000
  while (start + duration > Date. now()) {

  } // 2000之后
}

const works = [
  () => {

    console.log("第1个任务开始");
    sleep(0);//sleep(20);
    console.log("第1个任务结束");

  }, 
  () => {

    console.log("第2个任务开始");
    sleep(0);//sleep(20);
    console.log("第2个任务结束");

  }, 
  () => {

    console.log("第3个任务开始");
    sleep(0);//sleep(20);
    console.log("第3个任务结束");

  }, 
]; 
// 告诉浏览器, 你可以再空闲的时间执行任务,但是如果已经过期了,无论有没有空,都要帮我执行 workLoop
requrequestIdleCallback(workLoop, {timeout: 1000}); 

function workLoop(deadLine){
  console. log('本帧的剩余时间', parseInt(deadLine. timeRemaining()))  // 10ms
  // 如果还有剩余时间,并且还有没执行完的任务
  while((deadLine. timeRemaining() > 0 || deadline. didTimeout) && works. length > 0) {

    // 第一个任务执行完 花了 20ms
    // 执行每个工作单元
    performUnitOfWork();

  }
  // 如果时间到期了,就等待着下次调度,开辟一个新的时间片
  if (works. length > 0) {

    console.log(**只剩下${parseInt(deadline.timeRemaining())}ms,时间片到了等待下次空闲时间的调度**)
    requestIdleCallback(workLoop)

  }
}

// 取出每个空闲任务执行单元
function performUnitOfWork() {
  works. shift()(); // 每次取出完第一个,都剔除数组
}

# 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节点,占用着浏览器资源,积极地浪费性能,造成卡顿现象,且协调阶段是不能被打断的

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 树的基础上增加额外的信息生成来的,它本质来说是一个链表。

由Vdom生成Fiber树阶段

# commit提交阶段

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

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

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

# 细节拓展

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

<div>
  <A1>

    <B1>
      <C1></C1>
      <C2></C2>
    </B1>
    <B2></B2>

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

render节点遍历规则

# commit阶段,是如何commit的?

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

commit计算规则