「JS-Learning」事件循环机制,微任务和宏任务的关系

JavaScript(后面简称 JS)是单线程的,同一时间只能做一件事情。如果碰到某个耗时长的任务(比如一个需要 3s 的网络请求),那么后续的任务都要等待,这种效果是无法接受的,这时我们就引入了异步任务的概念。

所以 JS 执行主要包括同步任务和异步任务:

同步任务:会放入到执行栈中,他们是要按顺序执行的任务;

异步任务:会放入到任务队列中,这些异步任务一定要等到执行栈清空后才会执行,也就是说异步任务一定是在同步任务之后执行的。

本文主要讲的是 JS 的事件循环机制,它主要与异步任务有关。

二、任务队列

事件循环主要与任务队列有关,所以必须要先知道宏任务与微任务。

在任务队列中,有两种任务:宏任务和微任务。

宏任务:script标签中的整体代码、setTimeout、setInterval、setImmediate、I/0、UI渲染

微任务:process.nextTick(Node.js)、promise、Object.observe(不常用)、MutationObserver(Node.js)

任务优先级:process.nextTick > Promise.then > setTimeout > setImmediate

以上这些是常见的宏任务和微任务,记住就行了,不用追究为什么它是宏任务或微任务,因为就是这样的。

三、事件循环

那么什么是事件循环机制呢?

  • 一开始整个脚本(script标签中的整体代码)作为一个宏任务执行;
  • 执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列;
  • 当前宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行);
  • 当前宏任务执行完毕,开始检查渲染,然后 GUI 线程接管渲染(浏览器会在两个宏任务交接期间,对页面进行重新渲染);
  • 渲染完毕后,JS 线程继续接管,开始下一个宏任务(从任务队列中获取),依此循环,直到宏任务和微任务队列都为空。

上面这一过程就称为:事件循环(Event Loop)。

说的通俗一点:微任务是跟屁虫,一直跟着 当前 宏任务后面:代码执行过程中,每当碰到一个微任务,就马上跟在当前宏任务后面;当碰到一个宏任务,那不好意思你排到下一次循环再说。

图解 JavaScript 事件循环

四、实例验证

我们执行如下一段代码,用上面的思路执行,看一下结果是否和预期的一致。

console.log('script start')

// 宏任务
setTimeout(() => {
  console.log('setTimeout')
}, 0)

// 微任务 跟在当前宏任务后面
new Promise((resolve) => {
  console.log('new Promise')
  resolve()
  console.log('promise body')
}).then(() => {
  console.log('promise.then 1')
}).then(() => {
  console.log('promise.then 2')
})

console.log('script end')

按照上面的思路,我们来理一下,预测一下执行结果,看看实际效果是否是这样的。

执行流程:

  • 第一次事件循环
    • 首先这一整段 JS 代码作为一个宏任务先被执行
    • 遇到 console.log('script start') ,打印出 “start”;
    • 遇到 setTimeout ,回调函数作为宏任务压入到宏任务队列中,此时宏任务队列: [setTimeout]
    • 遇到 new Promise ,由于 new 一个对象是瞬间执行的,不是异步,所以打印出 “new Promise”;
    • 继续执行,由于 Promise 中的异步逻辑在 then 里面,在 then 之前的都不是异步,所以打印出 “promise body”;
    • 遇到了第一个 .then ,它是个微任务,将它放入微任务队列,跟在当前宏任务(整体代码)后面,此时微任务队列: [promise 1]
    • Promise 的第一个 .then 还没执行,只是排好队伍了,因此继续往后,遇到 console.log('script end') ,打印出 “end”。
    • 执行第一个宏任务后的微任务
    • 执行 Promise 的第一个 .then ,打印出 “promise 1”,,此时微任务队列: []
    • 又遇到 .then ,它是个微任务,将它放入微任务队列,跟在当前宏任务(整体代码)后面,此时微任务队列: [promise 2]
    • 执行 Promise 的第二个 .then ,打印出 “promise 2”,此时微任务队列: []
    • 整体代码执行完,微任务队列也执行完,当前的事件循环结束。
  • 第二次事件循环
    • 执行 setTimeout 的回调,打印出 “setTimeout”。

预测打印结果:

script start
new Promise
promise body
script end
promise.then 1
promise.then 2
setTimeout

执行代码后可以发现,实际打印结果和预测一致。

五、复杂情况

如果遇到更复杂的场景,比如当前微任务里有微任务,微任务里有宏任务,多层嵌套的情况,只需记住一句话: 微任务跟在当前宏任务后面,执行完当前宏任务,微任务就跟上,然后再执行下一个宏任务

六、有什么用

除了在前端面试中,会问到关于事件循环、执行栈的问题,了解 JS 事件循环机制有没有实质的作用呢?

  • 以后我们在代码中使用 Promise,setTimeout 时,思路将更加清晰,用起来更佳得心应手;
  • 在阅读一些源码时,对于一些 setTimeout 相关的骚操作可以理解的更加深入;
  • 理解 JS 中的任务执行流程,加深对异步流程的理解,少犯错误。

七、总结

  • JS 事件循环总是从一个宏任务开始执行;
  • 一个事件循环过程中,只执行一个宏任务,但是可能执行多个微任务;
  • 执行栈中的任务产生的微任务会在当前事件循环内执行;
  • 执行栈中的任务产生的宏任务要在下一次事件循环才会执行。

最后的最后,记住,JavaScript 是一门单线程语言,异步操作都是放到事件循环队列里面,等待主执行栈来执行的,并没有专门的异步执行线程。

参考

《ES6 标准入门(第3版)》

MDN