如何分析宏任务和微任务的执行

1,088 阅读5分钟

现在面试当中都会问到宏任务和微任务的问题,最近也是学习了下,感觉茅塞顿开,主要是要了解如何分析的方法的方法,这样的话遇到什么场景都不怕。我们来看看这两个任务如果来的。

宏任务和微任务的由来

说起宏任务和微任务,不得不提起 Event Loop, 简单来说,整个事件循环中,任务队列会依次从队列头取出任务来执行,但由于 JS 是单线程的,页面的大部份任务是在主线程执行的,这些任务包括了:

  • 渲染事件(如解析 DOM、计算布局、绘制)
  • 用户交互事件(如鼠标点击、滚动页面、放大缩小等)
  • JavaScript 脚本执行事件;
  • 网络请求完成、文件读写完成事件。

这些任务都管理在任务队列里面,也被称作宏任务,但是整个事件循环中都用宏任务控制的话,其实满足不了一些时间粒度上的要求,宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合了,比如后面要介绍的监听 DOM 变化的需求,而网页是需要经常和用户做交互的,所以引入了微任务

微任务就是一个需要异步执行的函数,执行时机是在主函数执行结束之后,当前宏任务结束之前。每个宏任务都关联了一个微任务队列,且有且只有一个。

微任务产生的方式主要是两种:

  • 使用 MutationObserver 监控某个 DOM 节点,然后再通过 JavaScript 来修改这个节点,或者为这个节点添加、删除部分子节点,当 DOM 节点发生变化时,就会产生 DOM 变化记录的微任务。
  • 使用 Promise,当调用 Promise.resolve() 或者 Promise.reject() 的时候,也会产生微任务。

微任务的意义是减少更新时的渲染次数。因为根据 HTML 标准,会在宏任务执行结束之后,在下一个宏任务开始执行之前,UI 都会重新渲染。如果在 microtask 中就完成数据更新,当 macrotask 结束就可以得到最新的 UI 了。如果新建一个 macrotask 来做数据更新的话,那么渲染会执行两次

简单来说宏任务和微任务的搭配是,就是宏任务执行完成后,渲染引擎不会执行下一个宏任务,会执行微任务,保障了实时性的问题。

宏任务和微任务主要是下面几种:

  • MacroTask(宏任务): script 全部代码、setTimeout、setInterval,I/O、UI Rendering。
  • MicroTask(微任务):Process.nextTick(Node独有)、Promise、MutationObserver

现在我们基本了解了宏任务和微任务大概怎么回事,我们来看一些题分析宏任务和微任务的执行。

从题说起

其实分析宏任务微任务的关键是

  • 分拆一个个的宏任务
  • 分析同步任务,微任务
  • 是否接着有宏任务

以上循环进行指导分析完毕

先看这道只包含宏任务的例子:

console.log('script start')	
setTimeout(function(){
    console.log('settimeout')	
})
console.log('script end')

可以理解最外层就是 script 作为第一个宏任务开始执行

  1. 找到同步任务 script start 和 script end, 没有微任务
  2. setTimeout 作为下一个宏任务开始,只有同步任务 settimeout, 没有微任务,其他宏任务,结束
  3. 所以最后输出 script start -> script end -> settimeout

来一道有微任务的

console.log('script start')
let promise1 = new Promise(function (resolve) {
    console.log('promise1')
    resolve()
    console.log('promise1 end')
}).then(function () {
    console.log('promise2')
})
setTimeout(function(){
    console.log('settimeout')
})
console.log('script end')

依旧是外层 script 作为第一个宏任务执行

  1. script start 同步执行
  2. 这有一个比较有意思的同步任务,就是 new Promise 的时候是立即执行的,也就是说里面的 promise1 和 promise1 end 也是同步任务,resolve 不会阻塞
  3. 接着是最后的同步任务 script end
  4. 然后 promise1 代表的微任务开始执行,promise2,到这第一个宏任务执行完成
  5. setTimeout 作为第二个宏任务开始检查,同步任务 settimtout, 微任务无,结束
  6. 所以最后的打印结果是 script start->promise1->promise1 end->script end->promise2->settimeout

再来一道有 async/await 的, 这也是一道比较复杂的题

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    console.log('async2');
}
console.log('script start');
setTimeout(function() {
    console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});
console.log('script end');

外层 script 作为第一个宏任务执行

  1. 同步任务 script start 首先执行,
  2. 接着 async1 函数被执行,async1 里面同步任务 async start 开始执行,
  3. 然后 async2 被执行 async2 被打印
  4. promise 中的同步任务 promise1 开始执行
  5. 最后的同步任务,script end 结束了当前宏任务的同步任务
  6. 随后 async1 中的微任务 async1 end 和 promise 的微任务 promise2 一次执行
  7. setTimeout 作为下一个宏任务只有同步任务 setTimeout
  8. 所以输出结果是 script start -> async1 start -> async2 -> promise1 -> script end -> async1 end -> promise2 -> settimeout

这题的迷惑可能在于 async,其实 async 会返回一个 promise 对象,遇到 await 就返回,所以等 await 之后执行完成才会执行之后的,相当于 async1 内部同理是这样

Promise.resolve(async2()).then(() => {
	console.log('async1 end');
})

其实遇到分析宏任务微任务输出的主要是要明白如何分析,把同步任务和微任务,接下来执行的宏任务逐步分析,还是蛮清晰简单的,可以自己给自己出出题分析结果,再在控制台打印看下分析得对不对。