问题是什么?
我们先看一个例子:
https://claudiopro.github.io/react-fiber-vs-stack-demo/stack.html
在上面这张图片中,页面出现一卡一卡的现象。
为什么人眼会感觉到卡顿?
当浏览器每秒刷新的次数低于60hz人眼就会感知卡顿掉帧等情况。
FPS
浏览器每秒刷新的次数称为 FPS(frame per second)。
浏览器的一帧说的就是一次完整的重绘。
理论上FPS越高人眼觉得界面越流畅,在两次屏幕硬件刷新之间,浏览器正好进行一次刷新(重绘),网页也会很流畅,当然这种是理想模式, 如果两次硬件刷新之间浏览器重绘多次是没意义的,只会消耗资源,如果浏览器重绘一次的时间是硬件多次刷新的时间,那么人眼将感知卡顿掉帧等, 所以浏览器对一次重绘的渲染工作需要在16ms(1000ms/60)之内完成,也就是说每一次重绘小于16ms才不会卡顿掉帧。
实际上,对用户来说,不良的体验不只是视觉上表现为卡顿与掉帧,因为在浏览器中,JS 运算、页面布局和页面绘制都是运行在浏览器的主线程当中,他们之间是互斥的关系。通常这时,对于用户在输入框输入内容这个行为来说,就体现为按下了键盘按键但是页面上不实时显示输入。
用户的交互得不到及时的响应,这对网页的用户体验来说是非常不利的。
React 15架构
Reconciler 协调器
每当有更新发生时,Reconciler会做如下工作:
- 调用函数组件、或class组件的render方法,将返回的JSX转化为虚拟DOM
- 将虚拟DOM和上次更新时的虚拟DOM对比
- 通过对比找出本次更新中变化的虚拟DOM
- 通知Renderer将变化的虚拟DOM渲染到页面上
Renderer(渲染器)
- ReactDOM渲染器,浏览器环境渲染
- ReactNative渲染器,渲染App原生组件
- ReactTest渲染器,渲染出纯Js对象用于测试
- ReactArt渲染器,渲染到Canvas, SVG 或 VML (IE8)
在每16.6ms时间内,需要完成如下工作:
JS脚本执行 ----- 样式布局 ----- 样式绘制
对于React的更新来说,递归遍历应用的所有节点由于递归执行,计算出差异,然后再更新 UI。递归是不能被打断的,所以更新一旦开始,中途就无法中断。当层级很深时,递归更新时间超过了16ms,用户交互就会卡顿。
另一方面,递归的方式进行渲染,使用的是 JS 引擎自身的函数调用栈,当层级很多的,可能会出现爆栈(stack overflow)的错误。当然这是递归的另一个缺点,但并不是React要优化的主要原因。
如何解决?
既然递归的不可打断的计算更新阻塞了用户的交互,有没有可能做到这样的效果:当浏览器有空闲时间就正常进行计算更新,而当有其他优先级更高的事件需要响应时就先暂停计算,去响应优先级高的事件,响应结束之后再接着刚才的计算继续进行,直到结束。
React 16的设计思想
React 16实现的思路是这样的:将运算切割为多个步骤,分批完成。说在完成一部分任务之后,将控制权交回给浏览器,让浏览器有时间进行页面的渲染。等浏览器忙完之后,再继续之前未完成的任务。这就是React 16中的Fiber设计思想。
有了解题思路后,我们再来看看 React 16具体是怎么做的。
React 16的实现
为了加以区分,以前的 Reconciler 被命名为Stack Reconciler。Stack Reconciler 运作的过程是不能被打断的,必须一条道走到黑:
而 Fiber Reconciler 每执行一段时间,都会将控制权交回给浏览器,可以分段执行:
为了达到这种效果,就需要有一个调度器 (Scheduler) 来进行任务分配。任务的优先级有六种:
- synchronous,与之前的Stack Reconciler操作一样,同步执行
- task,在next tick之前执行
- animation,下一帧之前执行
- high,在不久的将来立即执行
- low,稍微延迟执行也没关系
- offscreen,下一次render时或scroll时才执行
优先级高的任务(如键盘输入)可以打断优先级低的任务(如Diff)的执行,从而更快的生效。
Fiber Reconciler 在执行过程中,会分为 2 个阶段。
- 阶段一,生成 Fiber 树,得出需要更新的节点信息。这一步是一个渐进的过程,可以被打断。
- 阶段二,将需要更新的节点一次过批量更新,这个过程不能被打断。
阶段一可被打断的特性,让优先级更高的任务先执行,从框架层面大大降低了页面掉帧的概率。
看一下React 16下的效果
https://claudiopro.github.io/react-fiber-vs-stack-demo/fiber.html
可以看到,动画变得顺滑很多。
接下来看下Fiber的具体细节。
Fiber的实现细节
Fiber Reconciler 在阶段一进行 Diff 计算的时候,会生成一棵 Fiber 树。这棵树是在 Virtual DOM 树的基础上增加额外的信息来生成的,它本质来说是一个链表。
Fiber的结构
每个Fiber节点有个对应的React element,多个Fiber节点是如何连接形成树呢?靠如下三个属性:
// 指向父级Fiber节点
this.return = null;
// 指向子Fiber节点
this.child = null;
// 指向右边第一个兄弟Fiber节点
this.sibling = null;
举个例子,如下的组件结构:
function App() {
return (
<div>
i am
<span>KaSong</span>
</div>
)
}
对应的Fiber树结构:
作为一种静态的数据结构,保存了组件相关的信息:
//