前言
周末在家学习李兵老师的《浏览器工作原理与实践》课程,结尾有六个加餐内容。其中一篇“加餐二|任务调度:有了setTimeOut,为什么还要使用rAF?”引起了我的思维发散。
- rAF是什么?
- 为什么rAF可以替代setTimeOut?
- 浏览器的任务调度是什么?
我总觉得,如果弄懂这几个问题,我对浏览器的工作原理会有质的理解。(可能并不会,试试才知道。)
于是我认真读完老师的文章,还查阅了一些资料,将这块相关的知识点总结了出来。
rAF
rAF是requestAnimationFrame的简写。
MDN的介绍:
window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。
当你准备更新动画时你应该调用此方法。这将使浏览器在下一次重绘之前调用你传入给该方法的动画函数 (即你的回调函数)。回调函数执行次数通常是每秒 60 次。
这样读来确实有点像定时,设置好重绘的延时,循环进行更新。不过,rAF将刷新的频率固定为每秒60帧。
上效果
叶一一感觉最近有点手头紧,所以利用周末的时间,到劳务市场找了一份兼职,帮新房刷墙。
已知一个新房大概10面墙,每面墙 包含13*20 块砖,叶一一每秒能刷60块。叶一一早上8点开始干活,中午12点休息,上午能刷完几面墙?
开完的,不必真的计算(喜欢数学的朋友可以算一算,不过我这里没有答案)。
大概的功能就是,我设计了一个包含 13*20个矩形的图案,使用rAF设置动画进行颜色设置,每秒刷新60个色块,实现如下,点击刷墙即可开启动画效果。
rAF和setTimeOut
这是什么组合?为什么会有rAF替代setTimeOut的说法?
说一段历史
其实就是原本动画使用setTimeOut定时去实现更新。浏览器供应商一合计,使用者每次用代码实现一遍定时功能多麻烦,为什么我们不提供一个API,我们还能为使用者优化一些东西。
于是rAF应用而生,它是用于动画的基本 API,无论是基于 DOM 的样式更改、画布还是 WebGL。
setTimeOut的表现
我感觉我可能遗漏了setTimeout的特别信息,于是我到MDN文档里,认真阅读了它的知识点,果然,在备注里找到了几条注意事项:
- 当前页面(或者操作系统/浏览器本身)被其他任务占用时,会导致定时器延时;
- 最小延时 >=4ms,在浏览器中,函数嵌套,或者是由于已经执行的 setInterval 的回调函数阻塞,那么setTimeout每调用一次定时器的最小间隔是 4ms;
- 未被激活的页面,定时最小延迟>=1000ms,主要是为了优化后台页面的加载损耗;
- 有最大值延时值。
我摸了摸下巴,好像明白了setTimeout定时导致页面卡顿的原因。
我还在张鑫旭大神的文章《CSS3动画那么强,requestAnimationFrame还有毛线用?》一文中找到了一个有趣的表格,关于页面处于闲置的时候,不同浏览器对于setTimeOut和rAF两个方式,设置定时间隔的表现:
浏览器 |
setInterval |
requestAnimationFrame |
IE |
无影响 |
暂停 |
Safari |
无影响 |
暂停 |
Firefox |
>=1s |
1s - 3s |
Chrome |
>=1s |
暂停 |
Opera |
无影响 |
暂停 |
rAF的表现
大佬的《requestAnimationFrame for Smart Animating》一文中是这样概括的:
- 浏览器可以优化,所以动画会更流畅;
- 非活动页面中的动画将停止,让 CPU 冷却;
- 对电池更友好。
仔细想想也是,浏览器供应商提供的支持性质的API,会针对当下的一些浏览器问题,做出优化。
取而代之
经过上面的一系列知识点陈列之后,结论也就来了。
rAF替代setTimeOut做动画定时重绘,基于几点:
- 并发动画优化为单个回流和重绘循环,从而获得更高保真度的动画;
- 非活动页面动画停止,更少的 CPU、GPU 和内存使用,从而延长电池寿命;
任务调度
重头戏来了,往往不常用的功能,尤其是原理的知识,比较难理解。这次也不例外,我将李兵老师的文章知识点整理加上一些个人理解汇总了下来。以下内容算是一个学习笔记吧。
任务调度是什么以及它是怎么工作的呢?这一切要先从事件循环系统说起。
消息循环系统
每个渲染进程都有一个主线程,当主线程忙着处理DOM,忙着计算重绘,忙着处理JS事件的时候,就需要一个系统帮忙排班了。这个系统就是消息循环系统。
工作内容
消息循环系统的工作内容具体是什么呢?
李兵老师做了以下总结:
- 使用单线程处理安排好的任务;
- 在线程运行过程中处理新任务;
- 处理其他线程发送过来的任务;
- 处理其他进程发送过来的任务。
安全退出
活干完的时候,怎么保证页面主线程能够安全退出呢?
Chrome是这样干的,页面主线程会设置一个退出标志的变量,在每次执行完一个任务时,根据这个退出标志判断是否退出。
小结
1、明确的任务,排好序,可以用单线程来按顺序处理;
2、在线程运行过程中,可以采用事件循环机制接收并处理新任务;
3、其他线程发送过来的任务,为了确定任务的执行顺序,采用消息队列的方式;
4、单消息队列,存在着低优先级任务会阻塞高优先级任务的情况。
小结中的前三项,都是采用什么策略解决什么问题,最后一个问题,只有问题内容,没有解决方案,如何解决任务优先级的问题呢?
这个问题就引出了今天的主要内容——任务调度。
单消息队列的队头阻塞问题
问题描述
渲主线程会按照先进先出的顺序执行消息队列中的任务,当需要处理的任务逐渐增多时,对应进程的主线程也变得越拥挤,于是就出现了低优先级任务会阻塞高优先级任务的情况,把这种情况称之为消息队列的队头阻塞问题。
Chromium 是如何解决队头阻塞问题
这里通过李兵老师参考Chromium 团队的解决方案,分析如何解决队头阻塞的问题,我将重点内容进行了抽离。
第一次迭代:引入一个高优先级队列
在渲染进程中引入一个任务调度器,负责从多个消息队列中选出合适的任务,通常实现的逻辑,先按照顺序从高优先级队列中取出任务,如果高优先级的队列为空,那么再按照顺序从低优级队列中取出任务。
结合任务调度器灵活地调度任务,可以让高优先级的任务提前执行,采用这种方式似乎解决了消息队列的队头阻塞问题。
但是高优先级的确认是分情况的,更多的时候需要保持其相对执行顺序,如果将用户输入的消息或者合成消息添加进多个不同优先级的队列中,那么这种任务的相对执行顺序就会被打乱,甚至前置事件还没完成后置事件就已经执行了。因此需要让一些相同类型的任务保持其相对执行顺序。
第二次迭代:根据消息类型来实现消息队列
解决上面提到问题,可以为不同类型的任务创建不同优先级的消息队列。也就是说根据消息类型来创建消息队列。
但是它的问题是,消息队列的优先级都是固定的,任务调度器会按照固定好的静态的优先级来分别调度任务。但是静态的优先级会导致新的问题。
在页面加载阶段,使用静态优先级策略,可能会导致页面的解析速度将会被拖慢。
第三次迭代:动态调度策略
既然静态优先级策略有短板,那么动态调整策略怎么样呢?
动态调整策略主要是在页面不同的生存周期阶段,采用不同的优先策略。
页面大致的生存周期大体分为两个阶段,加载阶段和交互阶段。
来看Chromium 在不同的场景下,是如何调整消息队列优先级的
这样的优先级策略更为合理。
第四次迭代:任务饿死
以上方案看上去似乎非常完美了,不过依然存在一个问题,那就是在某个状态下,一直有新的高优先级的任务加入到队列中,这样就会导致其他低优先级的任务得不到执行,这称为任务饿死。
Chromium 为了解决任务饿死的问题,给每个队列设置了执行权重,也就是如果连续执行了一定个数的高优先级的任务,那么中间会执行一次低优先级的任务,这样就缓解了任务饿死的情况。
总结
今天的学习清单有点分量:
- rAF是浏览器供应商提供的API,功能是实现逐帧动画,并尝试如何使用它设置动画;
- 理清了rAF可以setTimeOut的原因在于保持高保真度的动画和提升电池寿命;
- 单消息队列的队头阻塞问题的每一个解决方案中,采用不同的任务调度策略,更为合理的策略是动态调度测试配合解决任务饿死。
今天也是超值的一天。
文章里面可能有一些错误,因为阅读资料的时候,这块的文字实在是太多了。欢迎大家指出,感谢💐。