本文首发于个人博客 https://maclaren0920.github.io
为什么要写这篇文章
在目前的技术社区上已经有大量介绍javascript事件循环的文章,并且也有一些写的非常不错的。但是他们大多都是在基于js层面来分析事件循环机制,很少有基于浏览器的运行流程来分析的,这就造就了很多对事件循环不甚了解的同学在实际开发中并不懂得怎样去正确的使用事件循环的特性,所以就有了现在这篇文章,<font color='red'>本文将从javascript在浏览器中的运行流程来分析事件循环的运行机制</font>。
javascript事件循环
我们都知道javascript是一门单线程的语言,所有的代码都是按顺序执行的,前面的代码没有执行完后面的会一直等待中,这就会造成程序的阻塞。
先看下以下代码:
setTimeout(() => {
console.log(1);
}, 0);
Promise.resolve().then(() => {
console.log(2);
});
console.log(3);
按照代码执行顺序应该是打印: 1、2、3,
然而实际却是 3、2、1,
这完全不符合代码按顺序执行的逻辑呀!
基于以上代码执行的顺序,我们有必要理解以下概念:
- js代码分同步任务和异步任务
- js实现异步的方式是基于事件循环模型
javascript是一门单线程的语言,按照直觉代码就是一行一行执行的,这就是同步任务,没错,以上你认为不符合直觉的代码执行顺序,这其中就掺杂了异步任务。
我们先来理解下同步任务和异步任务:
- 同步任务
当我们早上早高峰去做地铁时,人比较多,人们是按顺序一个一个排队进入地铁,如果前面的人没有往前走,后面的人就必须一直等着。 - 异步任务
当前面排队的比较多,队伍走的比较慢时,你闲着也是闲着反正又走不动,于是你拿出手机打开微信给朋友发了条消息,然后打开微信公众号看了会文章。
理解以上概念我们再来回头看下之前的代码,很显然setTimeout和Promise是属于异步任务的行列。那么同样是异步任务为什么2在1之前打印呢?
微任务和宏任务
除了同步任务和异步任务的区分之外,异步任务还有更精确的区分:
- 微任务(micro-task) job
- 宏任务(macro-task) task
同步任务和异步任务都是由js引擎来调度管理的,在这其中维护了一组任务队列(Event Queue);当执行到setTimeout时会将回调放入到<font color='red'>宏任务队列</font>,当执行到Promise then方法时会将会回调放入到<font color='red'>微任务队列</font>,当同步任务执行完成之后,就会去任务队列中的读取异步任务拿出来放到主线程中依次执行,首先会将微任务队列清空,然后再读取宏任务队列。
到这里你应该应该清楚上述代码的执行顺序的原因,但是这只是基于代码层面的,实际开发中往往更加复杂,异步任务就只有微任务和宏任务吗?
思考下一下代码:
document.body.style.background = 'black';
Promise.resolve().then(() => {
document.body.style.background = 'red';
}).then(() => {
document.body.style.background = 'green';
});
requestAnimationFrame(() => {
document.body.style.background = 'orange';
});
setTimeout(() => {
document.body.style.background = 'blue';
}, 200);
以上代码依次将body背景色改色,在变成橙色的一瞬间,最终变成了蓝色,你可以将其copy到控制台执行试试看效果。
结合上述讲解最终变成蓝色是没问题的,但是为什么会先变成橙色再变成蓝色呢,在这之前的黑色、红色和绿色呢?
从浏览器渲染顺序看异步执行机制
以上代码的运行结果在这里需要结合浏览器的渲染顺序来理解它。
我们来分析一下:
首先将body背景色变成黑色,然后遇到Promise then方法,依次将body背景色改成红色、绿色,我们之前提到Promise then方法属于微任务,该任务会在宏任务执行之前被全部清空,然后是执行requestAnimationFrame方法将body背景色改成橙色,最后是setTimeout宏任务将背景色改成蓝色。
这是代码的执行顺序,为什么之前的设置的背景色没有生效呢,只有requestAnimationFrame和setTimeout设置的生效了呢,很显然之前设置的被覆盖掉了。
结合同步任务和异步任务的讲解,我们知道同步代码先执行,首先设置背景色为黑色,然后清空微任务队列依次设置背景为红色、绿色,然后执行了requestAnimationFrame设置背景色为橙色,最后setTimeout将背景色变成蓝色。requestAnimationFrame是在setTimeout之前执行的,最后才算执行setTimeout,很显然requestAnimationFrame的执行时机比setTimeout更靠前,但是为什么会有先变成橙色再变成蓝色闪现的效果呢?
原因是在requestAnimationFrame和setTimeout执行顺序之间还穿插了GUI渲染操作,也就是我们经常说的浏览器绘制,当requestAnimationFrame执行完之后浏览器进行GUI渲染重新绘制页面,然后再执行setTimeout方法将背景色改成蓝色。
requestAnimationFrame
上述提到异步任务中分微任务和宏任务,那么requestAnimationFrame是什么东西呢,它是属于微任务还是宏任务呢?为什么它在setTimeout之前执行呢?我们可以在MDN看到关于requestAnimationFrame的描述:
window.requestAnimationFrame()告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。
意思是说requestAnimationFrame是在浏览器绘制页面之前最后修改DOM元素的时机,文档中并未提到微任务和宏任务,这说明它并不属于这两者之间,它是独立于任务队列的,是由浏览器渲染进程来调度的,因为它独立于同步任务和异步任务,不存在同步异步阻塞的情况,所以一般实现动画效果使用它来实现比setTimeout更合适。
结合以上代码示例和讲解我们可以总结出javascript事件循环的执行顺序:
如上图所示,我将它分为两步,首先执行第一个宏任务,也就是script代码块,将script代码块中的同步任务放入主线程中执行,同步任务执行完成之后取出微任务中队列中的所有任务依次执行,然后执行requestAnimationFrame中的回调,其次是GUI渲染,最后执行setTimeout。第一步在脚本加载完成之后执行,其次不断循环第二步,这就是javascript事件循环的具体流程。
总结
结合上述讲解,最后结尾我们来总结一下:
- javascript是单线程语言,同步任务同步执行,异步任务执行异步执行
- 异步任务分微任务和宏任务
- requestAnimationFrame是独立于任务队列的,它是浏览重新绘制页面之前操作DOM的最后时机
参考
https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame
https://juejin.cn/post/6844903512845860872#heading-3