编者荐语:本文旨在帮助大家在闲暇时间掌握React16.8版本的重大性能优化之React Fiber。
Fiber出现的背景?
在早期的React版本中,也就是React16.8版本之前。
大量的同步计算任务
阻塞了浏览器的UI渲染。默认情况下,JS运算
、页面布局
和页面绘制渲染
都是运行在浏览器的主线程
当中,他们之间是互斥
的关系。
如果JS运算持续占用主线程,页面就没法得到及时的更新,当我们调用setState
更新页面的时候,React会遍历应用的所有节点,与老的dom节点进行diff算法的对比,最小代价更新页面,即使
这样,整个过程也是一气呵成,不能被打断
的,如果页面元素很多,整个过程占用的时间就可能超过16毫秒,出现掉帧的现象。
针对这一现象,React团队从框架层面对web页面的运行机制做了优化,此后,Fiber
诞生了。
说到16ms,我们来看这样的一个概念
屏幕刷新率
- 目前大多数设备的屏幕刷新率为60次/秒
- 浏览器的渲染动画或页面的每一帧的速率也需要跟设备屏幕的刷新率保持一致。
- 页面是一帧一帧绘制出来的,当每秒绘制的帧数(FPS)达到60时,页面是流畅的,小于这个值时,用户会感觉到卡顿。
- 每个帧的预算时间是16.66毫秒(1秒/60)
- 1s 60帧,所以我们书写代码时尽量不让一帧的工作量超过16ms
Fiber的诞生
解决主线程长时间被JS晕眩占用这一问题的基本思路,是将运算切割为多个步骤
,分批完成。也就是说在完成一部分任务之后,
将控制权交回
给浏览器,让浏览器有时间再进行页面的渲染。等浏览器忙完之后,再继续之前React未完成的任务。
旧版React通过递归
的方式进行渲染,使用的是JS引擎自身的函数调用栈,它会一直执行到栈空为止。
而Fiber
实现了自己的组件调用栈,它以链表的形式遍历组件树,可以灵活地暂停、继续和丢弃执行的任务。实现的方式是使用了
浏览器的requestIdleCallback
这一API。官方的解释是这样的:
❝
window.requestIdleCallback()会在浏览器
空闲时期
依次调用函数,这就可以让开发者在主事件循环
中执行后台
或优先级低
的任务,而且不会像对动画和用户交互这些延迟触发产生关键的事件影响。函数一般会按先进先调用的顺序执行,除非函数在浏览器调用它之前就到了它的超时时间。❞
requestIdleCallback的核心用法
- 希望快速响应用户,让用户觉得够快,不能阻塞用户的交互行为
- requestIdleCallback 使开发者能够在
主事件循环
上执行后台和低优先级
的工作,而不会影响延迟关键事件,例如动画和输入的响应 - 正常帧任务完成后
没超过16ms
,说明时间有赋予,此时就会执行requestIdleCallback
里注册的任务
requestIdleCallback执行流程
requestIdleCallback执行流程
Fiber是什么
Fiber是一个执行单元
Fiber是一个执行单元,每次执行完一个执行单元, React 就会检查现在还剩多少时间,如果没有时间就将控制权让出去
Fiber是一种数据结构
React目前的做法是使用链表, 每个 VirtualDOM 节点内部表示为一个Fiber
,它可以用一个JS对象来表示:
const fiber = {
stateNode, // 节点实例
child, // 子节点
sibling, // 兄弟节点
return, // 父节点
}
Fiber是一种数据结构
Fiber之前的协调阶段
- React 会
递归比对
VirtualDOM树,找出需要变动
的节点,然后同步更新它们。这个过程 React 称为Reconcilation(协调) - 在Reconcilation期间,React 会
一直占用
着浏览器资源,一则会导致用户触发的事件得不到响应, 二则会导致掉帧,用户可能会感觉到卡顿
let root = {
key: 'A1',
children: [
{
key: 'B1',
children: [
{
key: 'C1',
children: []
},
{
key: 'C2',
children: []
}
]
},
{
key: 'B2',
children: []
}
]
}
function walk(element) {
doWork(element);
element.children.forEach(walk);
}
function doWork(element) {
console.log(element.key);
}
walk(root);
❝
在Fiber出现之前,React会不断递归遍历虚拟DOM节点,占用着浏览器资源,积极地浪费性能,造成卡顿现象,且协调阶段是不能
被打断的
。❞
❝
Fiber出现之后,通过某些Fiber调度策略合理分配CPU资源,让自己的
协调阶段变成可被终端
,适时
地让CPU(浏览器)执行权,提高了性能优化。❞
Fiber执行阶段
每次渲染有两个阶段:Reconciliation(协调\render阶段)和Commit(提交阶段)
- 协调阶段: 这个阶段
可以被中断
, 通过Dom-Diff算法找出所有节点变更,例如节点新增
、删除
、属性变更
等等, 这些变更React 称之为副作用
(Effect) - 提交阶段: 将上一个阶段计算出来的需要处理的副作用(Effects)一次性执行了。这个阶段必须
同步
执行,不能被打断
简单理解的话
- 阶段1:生成Fiber树,得出需要
更新
的节点信息
。(可打断
) - 阶段2:将需要更新的节点一次性地
批量更新
。(不可打断
)
❝
Fiber的协调阶段,可以被优先级较高的任务(如键盘输入)打断。
阶段1可被打断的特性,让优先级更高的任务先执行
,从框架层面大大降低了页面掉帧的概率。❞
Fiber执行流程
render阶段
Fiber Reconciliation(协调) 在阶段一进行 Diff 计算的时候,会生成一棵 Fiber 树
。这棵树是在 Virtual DOM 树
的基础上增加额外的信息生成
来的,它本质来说是一个链表。
commit提交阶段
Fiber 树在首次渲染的时候会一次过生成。在后续
需要 Diff
的时候,会根据已有树和最新 Virtual DOM 的信息,生成一棵新的树。这颗新树每生成一个新的节点,都会将控制权交回给主线程,去检查有没有优先级更高的任务需要执行。如果没有,则继续构建树的过程。
❝
1.如果过程中有优先级更高的任务需要进行,则 Fiber Reconciler 会丢弃正在生成的树,在空闲的时候再重新执行一遍。
2.在构造 Fiber 树的过程中,Fiber Reconciler 会将需要更新的节点信息保存在Effect List
当中,在阶段二执行的时候,会批量更新相应的节点。❞
细节拓展
render阶段是如何遍历,生成Fiber树的?
<div>
<A1>
<B1>
<C1></C1>
<C2></C2>
</B1>
<B2></B2>
</A1>
</div>
- 从顶点开始遍历
- 如果有第一个儿子,先遍历第一个儿子
- 如果没有第一个儿子,标志着此节点遍历完成
- 如果有弟弟遍历弟弟
- 如果有没有下一个弟弟,返回父节点标识完成父节点遍历,如果有叔叔遍历叔叔
- 没有父节点遍历结束
render节点遍历规则
commit阶段,是如何commit的?
类比Git分支功能,从旧树中fork出来一份,在新分支进行添加、删除和更新操作,经过测试后进行提交。
❤️ 交流讨论欢迎关注公众号 「秋风的笔记」
「点赞、分享」是对作者最大的支持❤️