一段废话
我当我把v15.6.2大体的东西读得稍有心得的时候(当时选定React版本时其正陷于许可证争论),此时版本迭代已经到了16.8.6。
不得不感慨前端库迭代的速度,这一年间,React Fiber 和 React Hook特性正式发布了。工程化上React也放弃了Gulp构建转而换为了Rollup。
读完的时候很多东西已经落时,但是当我写到这里的时候心里也并没有后悔。把React大体的读一遍,一则算是个人对峰顶的一次仰望,二则也算是圆了自己充舒适区走出来的想法,三来React再更新,最核心的地方也算是没有白理解。
感慨完毕之后,这里主要是准备换地图继续看新特性了。目标版本是16.8.6。
在15.6.2的源码阅读过程中,学会了很多很多深入理解的小技巧,迷茫时候也曾参考别人的文章,无处参考时候也无数次硬啃过各个难点。回想之前种种,阅读源码这事,真的有些如鱼饮水冷暖自知。
故这里小小记录,以为记。
——(by 2019.7.15)
前言
这一小节想想了还是列一下想从v16中了解的部分。
- React新的工程架构
- React Fiber
- render 可以返回字符串,数组,数字
- Error Boundary
- 新的生命周期
- React Portal
- Fragement
- React Hook
这里最想知道的还是React的Fiber部分,在React15上实际已经有了相关代码,但是没有正式发布。
所以这一篇暂时从Fiber开始。
Fiber的动机
虚拟DOM的理念在设计上个人认为已经接近理想性能的极致了,但是后来它还是遇到了现实意义上的瓶颈——当应用越来越大,卡顿现象也就会多起来。
之前我们认真研读了React15.6.2的reconciler源码,以及Diff算法。基于这些理解,可以很容易明白在React15的机制中,这个相关的调和过程,实际是树状、一路递归到子节点的。虽然Diff算法已经设计得非常高效,但是依然会面对递归大型的树时候,因为密集的计算导致主线程堵塞,进而对动画相关处理无法响应,造成了卡顿。
除此以外,当应用响应用户交互时候,绑定的事件过多,基于V15版本关于这块回调是归总并顺序同步执行,如果事件回调耗时过久,导致后来页面触发的更新渲染过慢,如此再三累计起来,卡顿现象就会更为明显。
想要改变这个现状,就必须在数据结构上有所创新——树结构无法预测它后面会有多少层、每层也无法预知会有多少子节点。所以Fiber采用了扁平化的数据结构,这样操作长度就可以被预知,卡顿的现象就有了处理的余地。
概念上这块这里就不再继续说了,因为这里不想画图,所以给大家一个别人写的浅析。
这里提一下概念上的东西,v15的reconciler叫做stack reconciler,v16的reconciler叫做fiber reconciler。
Fiber的拆分
在React15中,调和的过程实质上是组件的Diff,最后批量进行patch的过程。
这个Diff是可以中间停下来的,而patch批量处理批量处理,当然也是可以停下的,但是这不会有什么实质意义。
就上一小节的Fiber点动机来说,V15中Diff是对一棵树的一个不可预见、不能停止的遍历、递归的diff,它如果面临了过于庞大的树,这个Diff就会长时间占用主进程,页面就会卡,所以对它的拆分,可能是最好的选择。
其次就是patch一次执行太多事情,可能也会导致丢帧,这里也应该是通过上层diff的拆分来是实现。
stack reconciler和fiber reconciler本质的区别在于,fiber reconciler不再像stack reconciler那样直接以树结构遍历实例树,而是改用链表的方式来实现它们。这个从树到链表的变化,是Fiber的本质改进。
说实话,将树改成链表,这实际上是一个有点反人类的做法,不过它确实能解决性能上的问题——树的递归是无法中途暂停的(因为上层递归操作依赖下级递归操作的返回值),但是链表可以。
但是不管是stack reconciler还是fiber reconciler,不变的是,依然是对组件树的遍历的实现。
ps: 如果这块大家理解上有困难,那么可能需要对浏览器的宏任务和微任务有基础认识。我们在react里面的各种diff操作以及事件的集中执行这些都是微任务的范畴,而浏览器的刷新则属于宏任务的范畴。浏览器每次执行宏任务之前都会确保所有的微任务被全部执行完毕。这块知识属于EventLoop的范畴。
Fiber的遍历
Fiber的遍历是异步的遍历。这个遍历其实非常有意思,这让我想起《javascript忍者秘籍第二版》里面提到的,使用generator函数来进行DOM树的遍历。这块的代码部分主要从书上直接抄过来。
1 | <script> |
上面的代码是一个简单的,同步递归。本质上,也就是stack reconciler采用的办法。
如果换成生成器是这样:
1 | function* DomTraversal(element){ |
这中间有什么区别呢?区别在于,for…of这个函数的实现。或者说再往根源上说,在于迭代器的本质。
迭代器会有next方法,当我们执行DomTraversal.next()
时候,返回值的类型是{done: boolean; value: any}
这样的结构。直到到达终点,这个done===true。
而for…of则是对迭代器的遍历。它相当于:
1 | while(!(let item = weaponsIterator.next()).done) { |
所以在这个异步的流程中,我们可以随自己心意,随时随地去执行next(),而不需要像同步代码那样,必须一口气,处理到底。
这中间,可以斡旋的余地,就完全可以满足Fiber这种需要了。
以上是原理性说明。并不是实际实现办法,仅作思路引导。实际上React内部的Fiber节点并不是利用这种方法实现。具体实现可以看看后面的文章。
Fiber节点
数据结构
1 | { |
tag类型相关可以参见这里。
基于Fiber的Diff算法-理论篇
在说Fiber的链表Diff实现之前,还是可以考虑一下v16版本之前的基于Tree的Diff算法。
在基于树的对比策略下,v15版本采用的更新策略是: 根据更新逻辑, 从VDOM上确定一个尽可能小范围但是包含所有更新的节点作为对比根节点,然后从根到child节点遍历进行组件对比、Tree对比、Element对比,完成之后执行Patch。关于这个环节更具体的细节可以参考之前的文章。
那么在Fiber下是怎样的操作呢?因为这里不再以Tree作为对比的数据结构,所以之前的Diff实现需要有新的实现(感觉思路上其实倒是类似)。
因为基于链表的设置,这里Diff可以不用尽可能的将对比根节点往下面层级缩小,每次的对比都可以直接从根节点开始,而且因为基于对链表的设计,没什么损耗。
Fiber和ReactElement
不管如何,当标记更新之后,还是要执行Patch处理,只有这样才会将计算结果反馈到UI,对比才有现实意义。
所以理解Fiber和ReactElement的对应是一个很重要的环节。
同时,他也是衔接v15和v16源码理解的重要线索。
这里它们之间关键联结在FiberNode三个属性上。当渲染的render阶段结束,commit阶段开始
1 | memoizedProps: 保存旧节点的ReactElement |
关于这个属性,还得牵扯到updateQueue。