深入浅出Event Loop
前言
在前端工作了也有三年了,一直没写过对于JavaScript的文章,对于JavaScript的一些底层原理也一直不是很明白,Event Loop虽然也大概地了解过,但仅限于了解,不能很说出其底层的运行机制。有感于自己对Event Loop的理解薄弱,决定全面性地剖析它的运行机制。这篇文章讲解的是Event Loop如何在浏览器中运行,以及其执行的时机。如有不对,请指出我的错误,我会及时作出改正。
首先我们要知道两点:
- JavaScript是单线程的语言
- Event Loop是JavaScript的执行机制
JavaScript事件循环
在理解JavaScript事件循环之前,我们先要知道一件事,JS分为同步代码和异步代码。
同步任务会在主线程(即js引擎线程)上执行,会形成一个执行栈
主线程之外,还存在着一个任务队列,异步任务会在运行完毕后且主线程已清空,就会往任务队列中置入一个事件回调。
其中异步代码分为宏任务和微任务
- 同步和异步任务分别进入不同的执行环境,同步进入主线程,异步进入Event Table并注册函数
- 当指定的事情(个人理解:异步任务本身)执行完成时,Event Table会将这个函数进入Event Queue
- 当主线程执行完毕之后,回去Event Loop去读取对应的函数,并进入主线程执行
- 上面的过程不断循环,这就是常说的Event Loop(事件循环)
看到这里,想必有些小伙伴还有些疑惑
-
异步任务里面的宏任务和微任务的优先级谁先呢
-
指定事件指的是什么呢?
宏任务和微任务的执行顺序
先给出答案:先执行宏任务,再执行微任务。
要理清这个问题,首先要知道JS在执行同步代码时,遇到异步任务的代码时,会将异步任务的函数注册到了Event Table里。
而Event Loop的异步任务中,宏任务事件会放到宏队列中(macrotask queue),微任务事件会放到微任务队列中(microtask queue);
在主线程的同步代码执行完后,会检测代码的微任务队列,按照先进先出的顺序执行。
清空完微任务队列之后,宏任务队列也是按照先进先出的顺序依次执行;
到这里可能有些小伙伴就弄混淆了,为什么是先执行微任务队列而不是宏任务队列。
原因是,JavaScript运行代码本身也是一个宏任务事件。
指定事件指的是什么呢
我的理解是任务本身。
异步任务本身是同步执行的,并且执行完毕后会将异步任务里面的函数提取出来,放置到Event Table内。
举个栗子:
setTimeout( function task(){
console.log('I am macrotask');
},2000);
requestAnimationFrame( function task(){
console.log('I am macrotask2');
});
setTimeout
函数执行后,将参数的task
函数放置进了Event Table,并等到setTimeout
在2000ms
后执行完毕,再将task
函数放置到macrotask
队列中;
如上面的代码可以佐证我的猜想,因为setTimeout在需要在2000ms
后才能执行完毕,而requestAnimationFrame
会在重绘前执行函数。所以打印的顺序会是如下所示
需要补充一点是,在我们印象中,setTimeout最小是4ms
html standard中提到
- If timeout is less than 0, then set timeout to 0.
- If nesting level is greater than 5, and timeout is less than 4, then set timeout to 4.
- Increment nesting level by one.
- Let task’s timer nesting level be nesting level.
大概意思就是:
如果 timeout 小于 0,则将 timeout 设置为 0。
如果嵌套级别大于5,并且延时小于4ms,则将最小延时设置为4ms。
但实际上setTimeout的最低时间在不同浏览器上表现不一致, 并不一定是4ms,以谷歌为例,当嵌套级别小于5时,最小延迟为1ms,当嵌套大于等于5时,最小延迟为4ms;
setInterval
setInterval和setTimeout的表现没有太大区别,区别在于后者是循环地将函数置入Event Queue中,如果主线程任务尚未结束,那么也会需要等待;
需要注意的时,假设setInterval的延时时间低于setInterval参数的fn的执行时间,那么就会导致在fn执行完毕之前,setInterval是不会将fn2(我们将下一次循环的fn命名为fn2)置入Event Queue中,这就会导致fn时间完全没有时间间隔;
让我们来做一下题,看看我们把握到什么程度了
console.log('script start');
new Promise((resolve, reject) => {
console.log('promise1')
resolve()
}).then(() => {
console.log('then 1')
}).then(() => {
console.log('then 2')
})
Promise.resolve().then(() => {
console.log('promise2')
})
let _promise = () => new Promise((resolve, reject) => {
console.log('promise3')
})
setTimeout(() => {
console.log('setTimeout1')
Promise.resolve().then(() => {
console.log('promise4')
})
}, 100)
setTimeout(() => {
console.log('setTimeout2')
}, 0)
_promise().then(() => {
console.log('then3')
})
console.log('script end');
最后输出的结果是 :
**script start promise1 promise3 script end then 1 promise2 then 2 setTimeout2 setTimeout1 promise4**
结束语
以上,是我对网上一些已有的文章,以及与一位朋友交流后得出的理解,本文章是我对基于**参考文献[1]**文章一些精读和延伸;
感谢各位的小伙伴的阅读,也再次感谢朋友的交流和各位前辈的文章,让对Event Loop的我豁然开朗,才能借花献佛地分享给各位;
后续
有兴趣了解js运行机制的小伙伴,我推荐阅读参考文献[4]中的文章,这篇文章阐述了浏览器对于js的解析流程,以及js运行机制和浏览器的各个线程是如何协作的。
作者:juanfu
参考文献:
[1] 张倩qianniuer JS事件循环机制(event loop)之宏任务/微任务 [OL]
[2] 热情的刘大爷 性能优化篇 - js事件循环机制(event loop) [OL]
[3] weixin_48726650 为什么 setTimeout 有最小时延 4ms ? [OL]
[4] 「硬核JS」一次搞懂JS运行机制 [OL]
ps: 关于将函数置入event table,实际就是将函数体作为回调函数放入其中