深入浅出Event Loop

518 阅读5分钟

深入浅出Event Loop

前言

在前端工作了也有三年了,一直没写过对于JavaScript的文章,对于JavaScript的一些底层原理也一直不是很明白,Event Loop虽然也大概地了解过,但仅限于了解,不能很说出其底层的运行机制。有感于自己对Event Loop的理解薄弱,决定全面性地剖析它的运行机制。这篇文章讲解的是Event Loop如何在浏览器中运行,以及其执行的时机。如有不对,请指出我的错误,我会及时作出改正。

首先我们要知道两点:

  • JavaScript是单线程的语言
  • Event Loop是JavaScript的执行机制
JavaScript事件循环

在理解JavaScript事件循环之前,我们先要知道一件事,JS分为同步代码异步代码

同步任务会在主线程(即js引擎线程)上执行,会形成一个执行栈

主线程之外,还存在着一个任务队列,异步任务会在运行完毕后且主线程已清空,就会往任务队列中置入一个事件回调。

其中异步代码分为宏任务微任务

cmd-markdown-logo

  • 同步和异步任务分别进入不同的执行环境,同步进入主线程,异步进入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,并等到setTimeout2000ms后执行完毕,再将task函数放置到macrotask队列中;

如上面的代码可以佐证我的猜想,因为setTimeout在需要在2000ms后才能执行完毕,而requestAnimationFrame会在重绘前执行函数。所以打印的顺序会是如下所示

image-20210625010011872.png

需要补充一点是,在我们印象中,setTimeout最小是4ms

html standard中提到

  1. If timeout is less than 0, then set timeout to 0.
  2. If nesting level is greater than 5, and timeout is less than 4, then set timeout to 4.
  3. Increment nesting level by one.
  4. 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

邮箱:1390996852@qq.com

参考文献:

[1] 张倩qianniuer JS事件循环机制(event loop)之宏任务/微任务 [OL]

[2] 热情的刘大爷 性能优化篇 - js事件循环机制(event loop) [OL]

[3] weixin_48726650 为什么 setTimeout 有最小时延 4ms ? [OL]

[4] 「硬核JS」一次搞懂JS运行机制 [OL]

ps: 关于将函数置入event table,实际就是将函数体作为回调函数放入其中