关于eventLoop(事件循环机制)究竟是微任务还是宏任务先执行这件事

208 阅读5分钟

前言

最近逛掘金的时候,刷到了一位博主入职的面试过程。面试中有这么一个问题:事件循环机制中是微任务还是宏任务先执行。博主的回答是宏任务先执行,随后遭到了面试官的质疑。于是博主在面试结束后抱着验证的态度问了chatgpt。

博主首先让AI讲一下对eventLoop的理解:

image.png

按照这个说法看着像是先执行微任务,于是博主又问:是宏任务先执行的吧?

image.png

没想到chatpgt还是回答微任务先执行的,博主这下忍不住了,直接问:你确定吗?

image.png

没想到啊没想到,该AI直接改口了,看到这给我逗乐了。于是我抱着对真相无比渴望的态度去查了这个知识,终于大彻大悟了,下面是我的一些总结。

探讨

首先,我们来看一下什么是事件循环机制。

事件循环机制概念

说到事件循环机制,就不得不提到同步与异步。因为JavaScript是单线程的,所有的任务都需要排队,前一个任务执行完才能执行下一个任务,但是如果前一个任务执行的时间比较长(比如读取文件或者执行ajax操作等等),都会造成较长时间的等待,如果这个时候是同步的话,用户就只能在那干等着,严重影响用户体验。 因此,在设计JavaScript的时候就考虑到了这个问题,主线程可以不用等待ajax执行完毕然后返回数据,可以先挂起处于等待中的任务,然后执行后面的其他任务,直到ajax返回了结果,他才继续回头执行被挂起的任务。因此就有了同步和异步的出现。

然后呢,异步任务又分为了微任务和宏任务。

  • 微任务主要包括以下部分: script(整体代码)、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、setImmediate(Node.js 环境)
  • 宏任务主要包括以下部分:Promise.then、Promise.catch、MutaionObserver、process.nextTick(Node.js 环境)

事件循环的步骤如下:

  1. 首先,整体的script(作为第一个宏任务)开始执行的时候,会把所有代码分为同步任务、异步任务两部分
  2. 同步任务会直接进入主线程依次执行
  3. 异步任务会再分为宏任务和微任务
  4. 宏任务进入到Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中
  5. 微任务也会进入到另一个Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中
  6. 当主线程内的任务执行完毕,主线程为空时,会检查微任务的Event Queue,如果有任务,就全部执行,如果没有就执行下一个宏任务

上述过程会不断重复,这就是Event Loop,比较完整的事件循环。

这是事件循环机制的概念。接下来我们接着看一段代码。通过代码我们来继续探讨宏任务还是微任务先执行这个问题。

代码层面剖析

console.log(1); // 直接执行

setTimeout(function() {
    console.log(2); // 进入宏任务队列
})

new Promise(function(resolve) {
    console.log(3); //直接执行
    resolve()
}).then(function() {
    console.log(4); // 进入微任务队列
})

console.log(5); // 直接执行

该代码的打印顺序是啥呢?

没错!顺序就是1=>3=>5=>4=>3。

可是为啥呢?

通过上面的事件循环机制的概念,我们可以来判断:

  1. console.log(1) 是同步任务,直接执行,打印1;
  2. setTimeout(function() { console.log(2);}) 是宏任务,加入宏任务队列;
  3. new Promise(function(resolve) { console.log(3); resolve() }).then(function() { console.log(4); }) 该部分因为Promise本身是同步的,所以 console.log(3) 会直接执行,打印3,而.then()是个微任务,会加入微任务队列;
  4. console.log(5)又是一个同步任务,所以会直接执行,打印5
  5. 回过头来,同步代码执行完毕后,回去执行微任务,所以会打印4,
  6. 微任务执行完毕,执行宏任务代码,打印2

这么看来是不是像先执行了同步任务,又执行了微任务,最后执行了宏任务。

其实客观上来讲,是宏任务先执行的。本身顺序就是 宏-微-宏-微-宏-微-宏-微 这样交替着来的。入口肯定是宏任务,然后在入口代码中有创建微任务和宏任务的代码,入口代码执行完之后,立即执行微任务,然后再取队列里的下一个宏任务。
好多人理解的先执行微任务,再执行宏任务,这是相对于当前宏任务执行完毕之后这一个时刻来说的,当前宏任务执行完毕之后,会先清空微任务,再去执行下一个宏任务。所以是“先微任务,再宏任务”。
相对于整个流程开头来说的,执行完一个宏任务后立即再去清空微任务,这样完成一套事件的过程,是“先宏任务,再微任务”。
两种说法都没问题,你要说哪个描述更准确,那我更偏向于“宏+微是一体”的“先宏后微”的描述,毕竟“先微后宏”的描述要强调是在当前宏任务执行完这个时间点。

结论

要判定是微任务还是宏任务先执行是分情况的:

  • 如果在事件循环中,宏任务先执行
  • 如果放在代码层面,微任务先执行

参考: