系列文章
React Fiber 源码分析 第一篇 React Fiber 源码分析 第二篇(同步模式)React Fiber 源码分析 第三篇(异步状态)React Fiber 源码分析 第四篇(归纳总结)
前言
React Fiber 是 React 在 V16 版本中的大更新,利用了闲余时间看了一些源码,做个小记录~
什么是 Fiber
从开发者角度来看
实际上这次更新对于我们来说影响并不大,只是几个生命周期改变了(React 在版本中的更新简直做到了像一门语言一样,完美的兼容老版本,底层算法的大重构对于开发者来说完全透明),新引入的两个生命周期函数 getDerivedStateFromProps,getSnapshotBeforeUpdate 以及在未来 v17.0 版本中即将被移除的三个生命周期函数 componentWillMount,componentWillReeiveProps,componentWillUpdate,目前版本并不会影响原生命周期的使用,但不能和新的生命周期一起使用,也会被标记为不安全,下图为目前 React 的流程图
其他的几乎没有任何影响,我们还是照常的写着原来的代码,然后我们就感觉到网页性能更高了一些。
为什么网页性能会变高
要回答这个问题,需要回头看 javascript 是单线程的知识点。
单线程一次只能做一件事,在原来的 React 中,如果一次更新的时间比较长,那么用户就会感觉到卡顿,也就是丢帧了。
打个比方,假如我现在要更新 1000 个组件(往大了说),每个组件平均花时间 1ms,那么在 1s 内,浏览器的整个线程都被阻塞了,这时候用户在 input 上的任何操作都不会有反应,等到更新完毕,界面上突的一下就显示了原来用户的输入,这个体验是非常差的。这里借用官方一张图,Fiber 之前的版本就是这样,调用栈非常深
那么 Fiber,现在是怎么做呢?
Fiber 实际上是把一次更新拆成一个个的单元任务,每次做完一个单元任务后,就询问是否有更高的优先级任务,有就去执行,回头再来干这件事,如图
那么就明白了,Fiber 是一个任务调和器!,同样,我们根据这个来分析 Fiber 具体做了什么
Fiber 具体做了什么
首先,要做到这样的效果,那么就需要有以下的功能:
任务可分片(拆分任务)
任务可中断(执行另一个任务后,可以回头继续执行未完成的任务)
具备优先级(哪个任务先执行)
任务可分片
在 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 // 子任务片的优先级
任务中断再执行的流程
通过深度遍历搜索算法对每一个 fiber 即任务碎片进行更新
每一个任务碎片完成后会将 expirationTime 设为 NoWork
假设此时有更高优先级的任务,则执行更高优先级任务
任务执行完成后,会从任务列表中剔除,并继续执行其他未完成且可以执行的任务。
回到被打断任务,可以通过任务的 finishWork 属性判断是否需要执行更新
根据任务碎片的 expirationTime 判断是否需要执行更新
中断更新阶段其他属性介绍
Alternater
每次更新都不会对 fiber 直接操作,而是克隆一个作为 alternater 属性
updateQueue
更新队列,存放更新的信息
Effect
收集更新信息,生成真实 DOM
具备优先级
每个 Root 任务更新任务 fiber 都具有 expirationTime 属性,该属性即为优先级 expirationTime 越小,优先级越高,同步模式下该值为 0,每个层级的任务都是以链表的形式存在
为什么采用时间作为优先级属性
这时候就是 requestIdleCallback 这个 API 的骚操作了,这个 API 是干嘛的呢?
window.requestIdleCallback() 会在浏览器空闲时期依次调用函数,这就可以让开发者在主事件循环中执行后台或低优先级的任务,而且不会对像动画和用户交互这样延迟触发而且关键的事件产生影响。函数一般会按先进先调用的顺序执行,除非函数在浏览器调用它之前就到了它的超时时间。
也就是说 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 的源码中使用了很多链式结构,回调链,任务链等,这个主要是为了增删时性能比较高