现在面试当中都会问到宏任务和微任务的问题,最近也是学习了下,感觉茅塞顿开,主要是要了解如何分析的方法的方法,这样的话遇到什么场景都不怕。我们来看看这两个任务如果来的。
宏任务和微任务的由来
说起宏任务和微任务,不得不提起 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 作为第一个宏任务开始执行
- 找到同步任务 script start 和 script end, 没有微任务
- setTimeout 作为下一个宏任务开始,只有同步任务 settimeout, 没有微任务,其他宏任务,结束
- 所以最后输出 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 作为第一个宏任务执行
- script start 同步执行
- 这有一个比较有意思的同步任务,就是 new Promise 的时候是立即执行的,也就是说里面的 promise1 和 promise1 end 也是同步任务,resolve 不会阻塞
- 接着是最后的同步任务 script end
- 然后 promise1 代表的微任务开始执行,promise2,到这第一个宏任务执行完成
- setTimeout 作为第二个宏任务开始检查,同步任务 settimtout, 微任务无,结束
- 所以最后的打印结果是 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 作为第一个宏任务执行
- 同步任务 script start 首先执行,
- 接着 async1 函数被执行,async1 里面同步任务 async start 开始执行,
- 然后 async2 被执行 async2 被打印
- promise 中的同步任务 promise1 开始执行
- 最后的同步任务,script end 结束了当前宏任务的同步任务
- 随后 async1 中的微任务 async1 end 和 promise 的微任务 promise2 一次执行
- setTimeout 作为下一个宏任务只有同步任务 setTimeout
- 所以输出结果是 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');
})
其实遇到分析宏任务微任务输出的主要是要明白如何分析,把同步任务和微任务,接下来执行的宏任务逐步分析,还是蛮清晰简单的,可以自己给自己出出题分析结果,再在控制台打印看下分析得对不对。