JS中的事件循环(浏览器)

当我们调用一个函数时,js会创建对应的执行上下文,这个执行上下文中存在着上层作用域的指向,方法的参数,这个作用域中定义的变量以及这个作用域的this对象。当这一系列方法依次调用的时候,因为js是单线程的,所以同一时间只能执行一个方法,于是其余的方法被排队在一个单独的地方——执行栈(stack)

当脚本被执行时,js就会将其中的同步代码按照执行顺序加入到执行栈中,然后从头开始解析,当解析到是一个方法时,那么js就会在执行栈中创建对应的执行上下文并开始解析其中的同步代码,当执行完成后并返回结果后,js就会弹出该方法的执行上下文并摧毁,然后开始执行之后的同步代码,直至执行栈中的代码执行完成。

那么如果js解析的时异步事件时,它是怎么处理的呢?

我们首先看一段代码

  1. console.log('1');
  2. setTimeout(function() {
  3. console.log('2');
  4. }, 0);
  5. Promise.resolve().then(function() {
  6. console.log('4');
  7. }).then(function() {
  8. console.log('5');
  9. });
  10. console.log('3');
  11. //打印的顺序为
  12. //1 3 4 5 2

按照之前的理论,浏览器按照代码的执行顺序执行代码,打印的结果应该为12453,那是因为代码中的setTimout 和 Primise是异步事件,当浏览器遇到异步事件时,浏览器主线程并不会等待其执行完成,而是将异步事件放入一个事件队列中,然后继续执行执行栈中的其他任务,被放入事件队列中的事件并不会马上执行其回调。而是等执行栈中的任务全部执行完毕后,主线程闲置时,主线程会查找事件队列有没有任务,如果有,取出第一个的事件回调并放入执行栈中执行其中的同步代码。

那为什么2在45后面?

不同的任务源会被分配到不同的 Task 队列中,任务源可以分为 微任务(microtask) 和 宏任务(macrotask)。在 ES6 规范中,microtask 称为 jobsmacrotask 称为 task,那么加入事件队列中的异步事件还会被js区分为是jobs还是task,并分别分配到 jobs Queuetask Queue,当主线程的执行栈为空时,先检查jobs Queue中是否有任务,如果有依次取出所有任务的并加入执行栈中执行,然后才会去执行当次事件循环中的task Queue

深入Event Loop 和 RAF 与 SetTimeout区别 - 图1

宏任务中包括了 script ,浏览器会先执行一个宏任务,接下来有异步代码的话就先执行微任务

深入Event Loop 和 RAF 与 SetTimeout区别 - 图2

所以正确的一次 Event loop 顺序是这样的

  • 执行同步代码,这属于宏任务
  • 执行栈为空,查询是否有微任务需要执行
  • 执行所有微任务
  • 必要的话渲染 UI
  • 然后开始下一轮 Event loop,执行宏任务中的异步代码,循环往复,直到两个 queue 中的任务都取完。

另外如果遇到process.nextTick,在微任务中总是发生在所有异步任务之前

关于requestAnimationFrame的理解

是什么?

  • requestAnimationFrame是H5新增加的API,window对象的一个方法window.requestAnimationFrame
  • 浏览器专门为动画提供的 API,让 DOM 动画、Canvas 动画、SVG 动画、WebGL 动画等有一个统一的刷新机制

特点
  • 按帧对网页进行重绘。该方法告诉浏览器希望执行动画并请求浏览器在下一次重绘之前调用回调函数来更新动画
  • 由系统来决定回调函数的执行时机
    • 显示器有固定的刷新率一般为60Hz或75Hz,也就是说每秒最多只能重绘65次或75次,比如显示器屏幕刷新率为 60Hz,使用requestAnimationFrame API,那么回调函数就每1000ms / 60 ≈ 16.7ms执行一次;如果显示器屏幕的刷新率为 75Hz,那么回调函数就每1000ms / 75 ≈ 13.3ms执行一次。
    • 通过requestAnimationFrame调用回调函数引起的页面重绘或回流的时间间隔和显示器的刷新时间间隔相同。所以 requestAnimationFrame 不需要像setTimeout那样传递时间间隔,而是浏览器通过系统获取并使用显示器刷新频率

用法

动画帧请求回调函数列表:每个 Document 都有一个动画帧请求回调函数列表,该列表可以看成是由<handle, callback>元组组成的集合。

  • handle 是一个整数,唯一地标识了元组在列表中的位置(相当与setTimeout的timer),cancelAnimationFrame()可以通过它停止动画
  • callback 是一个无返回值的、形参为一个时间值的函数(该时间值为由浏览器传入的从 1970 年 1 月 1 日到当前所经过的毫秒数)。
  • 刚开始该列表为空。

setTimeout和requestAnimationFrame

setTimeout

setTimeout 其实就是通过设置一个间隔时间来不断的改变图像的位置,从而达到动画效果的。但我们会发现,利用 setTimeout 实现的动画在某些低端机上会出现卡顿、抖动的现象。 这种现象的产生有两个原因:

  • setTimeout 的执行时间并不是确定的。在JavaScript中, setTimeout 任务被放进了异步队列中,只有当主线程上的任务执行完以后,才会去检查该队列里的任务是否需要开始执行,所以 setTimeout 的实际执行时机一般要比其设定的时间晚一些。
  • 刷新频率受 屏幕分辨率屏幕尺寸 的影响,不同设备的屏幕绘制频率可能会不同,而 setTimeout 只能设置一个固定的时间间隔,这个时间不一定和屏幕的刷新时间相同。

setTimeout执行原理

首先要明白,setTimeout 的执行只是在内存中对元素属性进行改变,这个变化必须要等到屏幕下次绘制时才会被更新到屏幕上。如果两者的步调不一致,就可能会导致中间某一帧的操作被跨越过去,而直接更新下一帧的元素。假设屏幕每隔16.7ms刷新一次,而setTimeout 每隔10ms设置图像向左移动1px, 就会出现如下绘制过程(表格):

  • 第 0 ms:屏幕未绘制, 等待中,setTimeout 也未执行,等待中;
  • 第 10 ms:屏幕未绘制,等待中,setTimeout 开始执行并设置元素属性 left=1px;
  • 第 16.7 ms:屏幕开始绘制,屏幕上的元素向左移动了 1px, setTimeout 未执行,继续等待中;
  • 第 20 ms:屏幕未绘制,等待中,setTimeout 开始执行并设置 left=2px;
  • 第 30 ms:屏幕未绘制,等待中,setTimeout 开始执行并设置 left=3px;
  • 第33.4 ms:屏幕开始绘制,屏幕上的元素向左移动了 3px, setTimeout 未执行,继续等待中;

从上面的绘制过程中可以看出,屏幕没有更新 left=2px 的那一帧画面,元素直接从left=1px 的位置跳到了 left=3px 的的位置,这就是丢帧现象,这种现象就会引起动画卡顿。

requestAnimationFrame

与 setTimeout 相比,rAF 最大的优势是 由系统来决定回调函数的执行时机。具体一点讲就是,系统每次绘制之前会主动调用 rAF 中的回调函数,如果系统绘制率是 60Hz,那么回调函数就每16.7ms 被执行一次,如果绘制频率是75Hz,那么这个间隔时间就变成了 1000/75=13.3ms。换句话说就是,rAF 的执行步伐跟着系统的绘制频率走。它能保证回调函数在屏幕每一次的绘制间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。

快慢因素

  • requestAnimationFrame受系统的绘制频率影响,即屏幕分辨率 和 屏幕尺寸
  • setTimeout 受任务队列和页面渲染有关
  1. setTimeout(() => {
  2. console.log(3)
  3. })
  4. requestAnimationFrame(() => {
  5. console.log(1)
  6. })
  7. Promise.resolve(4).then((res)=>{
  8. console.log(res)
  9. })
  10. console.log(2)
  11. //输出结果可能是 2 4 3 1 也可能是 2 4 1 3
  12. //如果绑定一个事件,触发重绘
  13. btn.addEventListener('click', () => {
  14. list.appendChild(box)
  15. setTimeout(() => {
  16. console.log(3)
  17. })
  18. requestAnimationFrame(() => {
  19. console.log(1)
  20. })
  21. Promise.resolve(4).then((res)=>{
  22. console.log(res)
  23. })
  24. console.log(2)
  25. })
  26. //输出结果是 2 4 1 3

1.如果系统绘制率是 60Hz,那么requestAnimationFrame回调函数就每16.7ms 被执行一次; !!!在执行栈中【没有任何】的同步任务或异步微任务时!!!!:

  • (1)没有页面重新渲染(0延迟),setTimeout()回调函数就会在4ms执行(就浏览器10ms)。此时setTimeout()比requestAnimationFrame快。
  • (2)有页面重新渲染(最小延迟16ms),setTimeout()回调函数就会在16ms执行. 此时setTimeout()比requestAnimationFrame快。

2.如果绘制频率是75Hz,那么requestAnimationFrame回调函数就每13.3ms 被执行一次

!!!在执行栈中【没有任何】的同步任务或异步微任务时!!!!:

  • (1)没有页面重新渲染(0延迟),setTimeout()回调函数就会在4ms执行(就浏览器10ms)。此时setTimeout()比requestAnimationFrame快。
  • (2)有页面重新渲染(最小延迟16ms),setTimeout()回调函数就会在16ms执行. 此时requestAnimationFrame比setTimeout()快。

如下图所示,是当页面重绘时的一个流程,先通过右边的通道进行重绘再去触发左边的异步任务

image-20210615101634546.png
image-20210615101935285.png
image-20210615102115156.png

S(Styles)是将DOM和CSSOM组合成Render树,计算样式树从DOM树根开始构建,遍历每一个可见的节点。

L(Layout)是渲染树运行布局计算的每个节点的几何体,布局是确定呈现树中所有节点的宽度、高度和位置,以及确定页面上每个对象的大小和位置的过程。回流是对页面的任何部分或整个文档的任何后续大小和位置的确定。

P(Paint)是将各个节点绘制在屏幕上,浏览器将在布局阶段计算的每个框转换为屏幕上的实际像素。绘画包括将元素的每个可视部分绘制到屏幕上,包括文本、颜色、边框、阴影和替换的元素(如按钮和图像)

当使用setTimeout时如果占用时间过长,一帧之内没有执行完,就会和持续到下一帧,所以会有卡顿,如果使用rAF就不会出现这种情况,所以事件循环不一定每轮都伴随着渲染,但是如果有微任务,一定会伴随着微任务执行

requestAnimationFrame的优势

CPU节能:使用 setTimeout 实现的动画,当页面被隐藏或最小化时,setTimeout 仍然在后台执行动画任务,由于此时页面处于不可见或不可用状态,刷新动画是没有意义的,而且还浪费 CPU 资源。而 rAF 则完全不同,当页面处理未激活的状态下,该页面的屏幕绘制任务也会被系统暂停,因此跟着系统步伐走的 rAF 也会停止渲染,当页面被激活时,动画就从上次停留的地方继续执行,有效节省了 CPU 开销。

函数节流:在高频率事件(resize,scroll 等)中,为了防止在一个刷新间隔内发生多次函数执行,使用 rAF 可保证每个绘制间隔内,函数只被执行一次,这样既能保证流畅性,也能更好的节省函数执行的开销。一个绘制间隔内函数执行多次时没有意义的,因为显示器每16.7ms 绘制一次,多次绘制并不会在屏幕上体现出来。

参考链接: