React Fiber源码分析 第四篇(归纳总结)

976 阅读6分钟

系列文章

React Fiber源码分析 第一篇
React Fiber源码分析 第二篇(同步模式)
React Fiber源码分析 第三篇(异步状态)
React Fiber源码分析 第四篇(归纳总结)

前言

React Fiber是React在V16版本中的大更新,利用了闲余时间看了一些源码,做个小记录~

什么是Fiber

从开发者角度来看

实际上这次更新对于我们来说影响并不大,只是几个生命周期改变了(React在版本中的更新简直做到了像一门语言一样,完美的兼容老版本,底层算法的大重构对于开发者来说完全透明),新引入的两个生命周期函数 getDerivedStateFromProps,getSnapshotBeforeUpdate 以及在未来 v17.0 版本中即将被移除的三个生命周期函数componentWillMount,componentWillReeiveProps,componentWillUpdate,目前版本并不会影响原生命周期的使用,但不能和新的生命周期一起使用,也会被标记为不安全,下图为目前React的流程图

image

其他的几乎没有任何影响,我们还是照常的写着原来的代码,然后我们就感觉到网页性能更高了一些。

为什么网页性能会变高

要回答这个问题,需要回头看javascript是单线程的知识点。

单线程一次只能做一件事, 在原来的React中, 如果一次更新的时间比较长,那么用户就会感觉到卡顿,也就是丢帧了。

打个比方, 假如我现在要更新1000个组件(往大了说),每个组件平均花时间1ms,那么在1s内,浏览器的整个线程都被阻塞了,这时候用户在input上的任何操作都不会有反应,等到更新完毕,界面上突的一下就显示了原来用户的输入,这个体验是非常差的。这里借用官方一张图, Fiber之前的版本就是这样,调用栈非常深

image

那么Fiber,现在是怎么做呢?

Fiber实际上是把一次更新拆成一个个的单元任务,每次做完一个单元任务后,就询问是否有更高的优先级任务,有就去执行,回头再来干这件事,如图

image

那么就明白了,Fiber是一个任务调和器!, 同样,我们根据这个来分析Fiber具体做了什么

Fiber具体做了什么

首先,要做到这样的效果,那么就需要有以下的功能:

  1. 任务可分片 (拆分任务)
  2. 任务可中断 (执行另一个任务后, 可以回头继续执行未完成的任务)
  3. 具备优先级 (哪个任务先执行)

任务可分片

在React中,无论是state还是props的更新, 最后都操作在JSX的标签上
利用这种天然友好的表达,直接把每一个标签当成一个任务分片如:div、p1、p2、span都是一个任务分片

<div>
  <p>p1</p>
  <p>
    <span>p2</span>
  </p>
</div>

当然, 还要从标签转换成VDOM,再转成Fiber,才是一个真正的任务片,如图:

fiber的数据结构

任务可中断

Fiber之前React是通过栈调度器进行递归更新,毕竟标签化是天然嵌套的,对递归友好,但是递归不好break和continue

从大递归到大循环

Fiber则是以链表的形式来进行逐步更新(深度优先遍历算法),链表对break和continue友好Fiber节点拥有return, child, sibling三个属性,分别对应父节点, 第一个孩子, 它右边的兄弟,


(图来自网络,侵删)

如何回到中断
任务中断,执行高优先级任务后如何回来被中断的任务

React内部维护一个任务链表,每次某个任务结束后都会删除已完成的任务并继续执行其他可执行的任务,每个任务都有一个finishedWork属性,如果该属性不为null,则说明更新完毕,只差commit render阶段

回到中断任务后,如何从中断的任务片开始

这个主要依赖于fiber中的两个属性expirationTime和childExpirationTime,当某个fiber被执行完毕后,会把expirationTime设为NoWork,即被打断后可以通过该属性判断任务碎片是否 需要执行

this.expirationTime = NoWork  // 任务优先级
this.childExpirationTime = NoWork // 子任务片的优先级
任务中断再执行的流程
  1. 通过深度遍历搜索算法对每一个fiber即任务碎片进行更新
  2. 每一个任务碎片完成后会将expirationTime设为NoWork
  3. 假设此时有更高优先级的任务,则执行更高优先级任务
  4. 任务执行完成后,会从任务列表中剔除,并继续执行其他未完成且可以执行的任务。
  5. 回到被打断任务,可以通过任务的finishWork属性判断是否需要执行更新
  6. 根据任务碎片的expirationTime判断是否需要执行更新
中断更新阶段其他属性介绍
Alternater

每次更新都不会对fiber直接操作,而是克隆一个作为alternater属性

updateQueue

更新队列, 存放更新的信息

Effect

收集更新信息,生成真实DOM

具备优先级

每个Root任务\更新任务\fiber都具有expirationTime属性,该属性即为优先级expirationTime越小,优先级越高,同步模式下该值为0, 每个层级的任务都是以链表的形式存在

为什么采用时间作为优先级属性

这时候就是requestIdleCallback这个API的骚操作了, 这个API是干嘛的呢?

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

image

也就是说React实际上利用这个API在浏览器空闲期执行任务, 而这个API的回调有个参数deadline , 当你超时的时候,无论是不是在空闲期都会执行该任务, 这也就解释了为什么React采用时间来做优先级

不过实际上, React并没有在版本中使用了这个API,而是通过requestAnimationFrame来hack,强行设置每一帧的到期时间为requestAnimationFrame回调函数的参数加上33ms

var animationTick = function (rafTime) {
    isAnimationFrameScheduled = false;
    ...
    ...
    // 每帧到期时间为33ms
    frameDeadline = rafTime + 33
    if (!isIdleScheduled) {
      isIdleScheduled = true;
      window.postMessage(messageKey, '*');
    }
  };

当然了, 分优先级是有一个无法避免的问题, 那就是当有无数的优先级更高的任务插进来, 就会形成饥饿现象,原有的任务会一直得不到机会执行

总结

React Fiber实际上就是一个任务调和器,它做到了将每一次更新切分成任务分片,从而拥有了可中断且有优先级的进行其他任务的功能。
在分析的过程中,发现了React的源码中使用了很多链式结构, 回调链,任务链等,这个主要是为了增删时性能比较高