首先可以看一个16版本前和16版本之后即fiber架构之后的动画对比demo
可以看到stack(fiber架构之前)的示例,动画卡顿、掉帧非常严重,而fiber示例则很丝滑,
要解释stack为什么卡顿,我们首先要了解浏览器的渲染机制
渲染性能
60fps 与设备刷新率
目前绝大多数设备的屏幕刷新率都为60次/秒,也就是屏幕会60秒刷新一次,低于60hz,人眼会明显感觉到卡顿、掉帧
那么屏幕的刷新率为60hz,则浏览器最理想的刷新频率也为60次/秒,即 60FPS,即每次屏幕刷新之间,浏览器进行一次重绘,当然这是理想的情况下,每一次刷新重绘称之为一帧,每一帧的预算为16ms+,但实际上浏览器还有许多工作要做,所以每次用户代码的工作要在10ms内完成,如果没有达到此预算,则帧率将下降,并且会出现卡顿
像素管道
在浏览器的每次绘制生命周期中我们需要了解并注意五个主要的区域,这些是我们拥有最大控制权的部分,也是像素至管道屏幕的关键点
· JavaScript:一般来说我们会使用javascript来实现一些视觉效果的变化,如弹窗、动画、切换路由等等等
· style(样式计算):此过程是根据匹配选择器,计算出哪些元素应用哪些css规则的过程
· Layout(布局):在知道一个元素对应哪些规则之后,浏览器即可开始计算它要占据的空间大小,及在屏幕中的位置,网页的布局意味着一个元素可能影响其它元素,
· Paint(绘制):绘制是填充像素的过程,它涉及绘出文本、颜色、图像、边框和阴影、基本上包含了元素的每个可视部分
· composite(合成):由于页面的各个部分可能被绘制到多层,由此它们需要按照正确的顺序绘制到屏幕上
这五个步骤的每一步都有可能产生阻塞,由此产生卡顿,所以有比较了解到我们的代码触发了哪些部分
1. JS / CSS > 样式 > 布局 > 绘制 > 合成
如果更改了元素的几何属性,那么浏览器必将检查所有元素,然后 "自动充排" 页面,任何受到影响的部分都将重绘,最终绘制的元素需要进行合成
2. js/css > 样式 > 绘制 > 合成
如果只修改 "paint only" 属性(例如:颜色、背景等),即不会影响到页面的布局,则浏览器将跳过布局
3. js/css > 合成
如果修改了一个及不需要布局也不需要绘制的属性,则浏览器将会直接进入合成
模拟demo中的stack
这里我们知道了 js的执行 页面的渲染是在同一条线程的,那么我们来实现一个动画,模拟一下上边的stack demo
<style>
@keyframes boxAn{
0%{
left:0px
}
25%{
left:50px
}
50%{
left:100px
}
75%{
left: 150px;
}
100%{
left: 200px;
}
}
.box{
width: 400px;
height: 100px;
position: absolute;
background: red;
display: flex;
flex-wrap: wrap;
animation: boxAn 3s ease infinite alternate;
}
.box div {
flex-shrink: 0;
width: 20px;
height: 20px;
background: pink;
}
</style>
<body>
<div class="box">
</div>
<div class="box2">
</div>
</body>
<script>
let box = document.querySelector('.box');
let left = 1
let fragment = document.createDocumentFragment()
for(let i = 0; i < 1000; i++) {
let div = document.createElement('div');
div.innerHTML = 1;
box.appendChild(div)
}
function sleep(date) {
let flag = true;
const now = Date.now();
while (flag) {
if (Date.now() - now > date) {
flag = false;
}
}
}
let begin = 0
let text = true
function func(){
setTimeout(() => {
let divs = document.querySelectorAll('.box div')
let number = begin > 10 ? (begin = 0) : begin++
divs.forEach(element => {
element.textContent = number
});
// sleep(500)
if(!(left > 500)) {
text = !text
++left
func()
}
},200)
}
func()
</script>
当没有调用sleep的时候 可以看到
除了开始初始化脚本的时候,FPS 一直是绿的 画面也很流畅,然后我们放开 sleep
可以看到此时的 FPS 基本为 1、2 动画非常卡,线程基本都被定时器任务占据.
这也正是 Dan Abramov 在演讲中讲到的:
When dealing with UIs, the problem is that if too much work is executed all at once, it can cause animations to drop frames… 当处理UIs时,问题是如果大量的工作一次性执行,那么就可能导致动画掉帧……
当react执行更新的时候,它同步遍历整个组件树,为每个组件执行渲染更新工作,于是当组件树很大的时候,就会造成代码执行的时间超过16ms,进而导致浏览器卡顿、掉帧
requestIdleCallback
其实对于上边的问题,浏览器也给出了解决方案,就是 requestIdleCallback,
api:
var handle = window.requestIdleCallback(callback[, options])
callback: 一个在事件循环空闲时即将被调用的函数的引用。函数会接收到一个名为 IdleDeadline 的参数,这个参数可以获取当前空闲时间以及回调是否在超时时间前已经执行的状态。
其中 IdleDeadline 对象包含:
didTimeout,布尔值,表示任务是否超时,结合 timeRemaining 使用。
timeRemaining(),表示当前帧剩余的时间,也可理解为留给任务的时间还有多少。
options的参数
timeout: 表示超过这个时间后,如果任务还没执行,则强制执行,不必等待空闲。尚未通过超时毫秒数调用回调,那么回调会在下一次空闲时期被强制执行。如果明确在某段时间内执行回调,可以设置timeout值。在浏览器繁忙的时候,requestIdleCallback超时执行就和setTimeout效果一样。
chrome给出的这个api的调用时间是这样的:
requestIdleCallback将在帧的末尾有空闲时间,或当用户不活动时调度工作。这意味着你有机会在不妨碍用户的情况下完成你的工作。
也就是说有两个场景会调用requestIdleCallback:
1. 当前浏览器一帧渲染时间小于屏幕刷新一次的时间(对于60hz的设备,也就是小于16ms),到下一帧渲染开始出现的空闲时间
注意:这里如果当前帧没有空闲时间,则不会调用
2. 当前浏览器没有渲染任务,主线程处于空闲状态,事件队列为空,为了避免在不可预测的任务(例如用户输入的处理)中引起用户可察觉的延迟,这些空闲周期的长度应限制为最大值50ms,也就是IdleDeadline.timeRemaining()最大不超过50,
**注意:**最大为50毫秒,是根据研究[ RESPONSETIME ] 得出的,该研究表明,对用户输入的100毫秒以内的响应通常被认为对人类是瞬时的,就是人类不会有察觉。将闲置截止期限设置为50ms意味着即使在闲置任务开始后立即发生用户输入,用户代理仍然有剩余的50ms可以在其中响应用户输入而不会产生用户可察觉的滞后。
这里有个总结:
requestIdleCallback会在每一帧结束后执行,去判断浏览器是否空闲,如果浏览器一直处于占用状态,则没有空闲时间,且如果requestIdleCallback没有设置timeout时间,那么callback的任务会一直推迟执行,如果在当前帧设置timeout,浏览器会在当前帧结束的下一帧开始判断是否超时执行callback。
此外浏览器是不推荐在 requestIdleCallback 中操作dom的:
1. 浏览器可能会因为太忙而无法在给定的帧中运行任何回调,所以你不应该期望在帧的末尾会有任何空闲时间来做更多的工作
2. 更改DOM会导致不可预测的执行时间,因为它会触发样式计算、布局、绘制和合成
3. 如果回调在帧末尾被触发,那么它将被安排在当前帧提交之后出发,这意味着更改的样式、布局将无效,如果下一帧有任何类型的布局读取,如:getBoundingClientRect、clientWidth等等,浏览器将不得不执行强制同步布局,这是一个潜在的性能瓶颈。
最佳实践是在requestAnimationFrame(告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画)回调内部进行dom更改,如果使用的是VDOM库,则可以使用requestIdleCallback进行更改,但是可以在下一个requestAnimationFrame回调而不是空闲回调中应用DOM补丁
其实react也是基于 requestAnimationFrame 做的 polyfill(看到其他文章介绍的,我还没有去阅读react的源码)
为什么fiber
首先让我们看一下之前的react工作原理
当react接收到一个更新时,他会逐层的进行构造,element => instances => domNode,并且是递归调用,直到组件树的底部,所如果在chrome的开发工具 timeLine(现在没有了)看到这些递归调用 会是一个这样的图像
所以当处于递归调用栈的时候,主线程就会被js阻塞,从而导致掉帧,就如上边的demo,
所以,问题是react不够快么?
不,不是的,用户的代码是react之外的,是不可控制的,所以是react更快是不足以解决这个问题
结合上边的背景,浏览器的重绘,被卡在了这些较大的更新之后,
解决方法就是播放更新,即结合来自浏览器的更新,如:css动画,浏览器大小调整,即通过优先级判断谁先更新
不同的更新有高低优先级之分,如高优先级:用户输入的响应,低优先级:从服务器获取数据
所以 fiber的就是把拆分任务,并确立优先级,此外它还追踪了当前经过的时间
使当前线程计算树的一小部分,然后返回检查是否有其它工作
fiber架构主要有两个阶段:reconciliation/render(协调),和commit(提交)
其中 reconciliation 是可中断的,commit阶段是不可中断的
在reconciliation阶段主要做以下几个事情
· 更新 state 和 props
· 调用生命周期钩子
· 父级组件中通过调用 render 方法,获取子组件 (类组件),函数组件则直接调用获取
· 将得到的子组件与之前已经渲染的组件比较,也就是 diff
· 计算出需要被更新的 DOM
新的协调算法的上下文就是一种被称为 fiber 的数据结构
filber只是一个简单地对象
结合fiber这种链表式的数据结构,react可以在任何时候通知遍历操作,也可以随时启动,再结合上边的 requestIdleCallback 就可以实现上图的调度工作
参考链接:
www.youtube.com/watch?v=ZCu…
developers.google.com/web/fundame…
developers.google.com/web/updates…
juejin.cn/post/684490…
juejin.cn/post/684490…
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!