React Fiber源码分析 (介绍)
写了分析源码的文章后, 总觉得缺少了什么, 在这里补一个整体的总结,输出个人的理解~
文章的系列标题为Fiber源码分析, 那么什么是Fiber,官方给出的解释是:
React Fiber是对核心算法的一次重新实现。
ummm, 这样说实在是有点泛,详细分析一下
先从开发者角度来看
实际上这次更新对于我们来说影响并不大,只是几个生命周期改变了,新引入的两个生命周期函数 getDerivedStateFromProps
,getSnapshotBeforeUpdate
以及在未来 v17.0 版本中即将被移除的三个生命周期函数componentWillMount,componentWillReeiveProps,componentWillUpdate,目前版本并不会影响原生命周期的使用,但不能和新的生命周期一起使用,也会被标记为不安全,下图为目前React的流程图:
其他的几乎没有任何影响,我们还是照常的写着原来的代码,然后我们就感觉到网页性能更高了一些。
为什么网页性能会变高,Fiber做了什么?
要回答这个问题,需要回头看javascript是单线程的知识点。
单线程一次只能做一件事, 在原来的React中, 如果一次更新的时间比较长,那么用户就会感觉到卡顿,也就是丢帧了。
打个比方, 假如我现在要更新1000个组件(往大了说),每个组件平均花时间1ms,那么在1s内,浏览器的整个线程都被阻塞了,这时候用户在input上的任何操作都不会有反应,等到更新完毕,界面上突的一下就显示了原来用户的输入,这个体验是非常差的。这里借用官方一张图, Fiber之前的版本就是这样,调用栈非常深
那么Fiber,现在是怎么做呢?
Fiber实际上是把一次更新拆成一个个的单元任务,每次做完一个单元任务后,就询问是否有更高的优先级任务,有就去执行,回头再来干这件事,如图
那么就明白了,Fiber是一个任务调和器!, 同样,我们根据这个来分析Fiber具体做了什么
Fiber具体做了什么
首先,要做到这样的效果,那么就需要有以下的功能:
1.可分片 (拆分任务)
2.可中断 (执行另一个任务后, 可以回头继续执行未完成的任务)
3.具备优先级 (哪个任务先执行)
想要做到拆分任务就需要任务可以分片,也就是React的Fiber,fiber即为一个分片任务,贴上数据结构:
可中断即是使用了队列的形式保存任务, 具体可以看源码~
基本是一个fiber即为一个组件,而优先级即使用fiber的expirationTime属性, expirationTime越小即优先级越高
function FiberNode(tag, pendingProps, key, mode) { // Instance this.tag = tag; this.key = key; this.elementType = null; this.type = null; this.stateNode = null; // Fiber this.return = null; this.child = null; this.sibling = null; this.index = 0; this.ref = null; this.pendingProps = pendingProps; this.memoizedProps = null; this.updateQueue = null; this.memoizedState = null; this.firstContextDependency = null; this.mode = mode; // Effects this.effectTag = NoEffect; this.nextEffect = null; this.firstEffect = null; this.lastEffect = null; this.expirationTime = NoWork; this.childExpirationTime = NoWork; this.alternate = null; }
从数据结构上, 有几个属性值得说一下,
首先是官方注释了Fiber的几个属性, 这几个是非常重要的
原来的React更新任务是采用递归的形式, 那么现在如果任务想中断, 在递归中是很难做处理的, 所以React改成了大循环的模式
修改了生命周期也是因为任务可中断~
具体可以参考下面这篇文章
到目前为止(React 16.4),React的渲染机制遵循同步渲染: 1) 首次渲染: willMount > render > didMount, 2) props更新时: receiveProps > shouldUpdate > willUpdate > render > didUpdate 3) state更新时: shouldUpdate > willUpdate > render > didUpdate 3) 卸载时: willUnmount 期间每个周期函数各司其职,输入输出都是可预测,一路下来很顺畅。 BUT 从React 17 开始,渲染机制将会发生颠覆性改变,这个新方式就是 Async Render。 首先,async render不是那种服务端渲染,比如发异步请求到后台返回newState甚至新的html,这里的async render还是限制在React作为一个View框架的View层本身。 通过进一步观察可以发现,预废弃的三个生命周期函数都发生在虚拟dom的构建期间,也就是render之前。在将来的React 17中,在dom真正render之前,React中的调度机制可能会不定期的去查看有没有更高优先级的任务,如果有,就打断当前的周期执行函数(哪怕已经执行了一半),等高优先级任务完成,再回来重新执行之前被打断的周期函数。这种新机制对现存周期函数的影响就是它们的调用时机变的复杂而不可预测,这也就是为什么”UNSAFE”。 --------------------- 作者:辰辰沉沉大辰沉 来源:CSDN 原文:https://blog.csdn.net/Napoleonxxx/article/details/81120854
什么是大循环?
即执行某个fiber后, 会执行他的子元素, 如果没有子元素, 则兄弟元素, 然后又回到父元素, 父兄弟元素...
而寻找元素则是根据其上面几个属性return(父元素),child(子元素),sibiling(兄弟元素)
假设有以下的代码:
<div> <span1></span1> <p> <span2><span2> </p> </div>
他的执行如图
Fiber的优先级?
再下来, fiber又是怎么做到根据优先级执行任务时不会卡顿呢,如果任务很多, 无穷无尽, 那不是一样会丢帧?
这时候就是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, '*'); } };
当然了, 分优先级是有一个无法避免的问题, 那就是当有无数的优先级更高的任务插进来, 就会形成饥饿现象,原有的任务会一直得不到机会执行
后面还有一个打了注释Effect标签的几个属性,这几个属性主要是收集每次更新的结果, 并在最后一层层往上迭代, 最后由最高的节点收集, 并执行更新。
在分析的过程中,发现了React的源码中使用了很多链式结构, 回调链,任务链等, 这个主要是为了增删时性能比较高
最后总结一下:
React Fiber实际上就是一个任务调和器,它做到了将每一次更新切分成任务分片,从而拥有了可中断且有优先级的进行其他任务的功能。
如果想看源码, 可以参考本系列的另外三篇文章