理解 JavaScript 中的 microtasks 和 macrotasks(微任务和宏任务)
如果一段JavaScript代码中包含了setTimeout
几乎所有的前端同学都知道其代码会被延迟(异步)执行,但是如果代码中同时出现了setTimeout
、await
以及Promise resolve
的话大家还能说出来他们的先后执行顺序么?先抛出一个网上流传的前端面试题,主要涉及的知识点是异步async/await
,setTimeout
,Promise resolve
的执行先后顺序,也就是我们这里要讨论的microtaks
(异步微任务)和macrotasks
(异步宏任务):
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("promise end");
});
console.log("script end");
如果想要完全解答出这个题目,需要了解JavaScript的同步队列和异步队列,以及异步队列中细分出的microtasks
(微任务)和macrotasks
(宏任务)
同步&异步
我们知道在JavaScript的运行过程中会优先执行同步的任务,如果遇到异步代码就会将异步代码放置到异步队列中,等同步代码执行完成之后再去执行异步队列中的代码。比如下面的代码:
console.log(1)
setTimeout(()=>{
console.log(2)
}, 0)
console.log(3)
setTimeout(()=>{
console.log(4)
}, 0)
console.log(5)
代码首先会执行第一行的console.log(1)
。继续执行遇到了第一个异步代码setTimeout
,此时JavaScript会把该片段代码放置到异步队列中(确切的说是macrotasks
,后面会讨论到),然后继续往下执行同步代码console.log(3)
,然后又遇到了第二个setTimeout
,同样将其放置到异步队列中继续执行同步代码console.log(5)
。当所有的同步代码执行完成之后,再回过来检查异步任务中是否有还未执行的任务,如果有就按照顺序执行,就是我们这里的分别打印2和4的setTimeout
两段代码了。
所以上面的代码的执行结果是:
1
3
5
2
4
异步中的优先级:microtasks & macrotasks
那么在异步任务中有没有优先级呢?我们先看如下的代码:
console.log(1)
setTimeout(() => {
console.log(2)
}, 0);
new Promise(function(resolve) {
console.log("3");
resolve();
}).then(function() {
console.log("4");
});
console.log(5)
聪明的你一定知道,代码中的console.log(1)
,console.log(5)
是同步代码,会优于异步代码执行,但是对于setTimeout
中的console.log(2)
、Promise
中的console.log("3")
和Promise resolve
中的console.log("4")
,到底是谁先执行呢?
这里我们要引入microtasks
(微任务)和macrotasks
(宏任务)的概念,microtasks
和macrotasks
都是异步的任务,但区别是:
microtasks
任务优先于macrotasks
任务,也就是说当同步任务执行完成之后会优先执行microtasks
中的任务,之后才轮到macrotasks
中的任务microtasks
的执行会优先于UI任务(实际上UI任务属于macrotasks
),意味着如果你不停的向microtasks
中插入任务就回导致页面停止响应- 常见的
microtasks
有:process.nextTick
,Promises
(其中Promises
的构造函数是同步的),queueMicrotask
,MutationObserver
- 常见的
macrotasks
有:setTimeout
,setInterval
,setImmediate
,requestAnimationFrame
,I/O
,UI rendering
所以对于上面的代码来说:
- 先执行同步代码
console.log(1)
- 遇到
setTimeout
放入macrotasks
中 - 遇到
Promise
执行同步的构造函数console.log("3"); resolve()
(Promise的构造函数是同步执行的!Promise的构造函数是同步执行的!Promise的构造函数是同步执行的!) 然后把then
放入到microstaks
中 - 继续执行同步代码
console.log(5)
- 同步代码执行完成,执行
microstaks
中的任务,也就是Promise.then
中的console.log("4")
microstaks
执行完毕,执行macrostaks
中的任务,即setTimeout
的console.log(2)
所以结果是:
1
3
5
4
2
异步中的async和await片段执行顺序
我们继续看例子:
async function async1(){
console.log(2)
await async2()
console.log(3)
}
async function async2(){
console.log(4)
}
console.log(1)
async1()
console.log(5)
Promise.resolve().then(() => console.log(6))
console.log(7)
Hmmm,让我们猜猜执行顺序:
- 首先是同步代码
console.log(1)
- 然后是 async1 中的同步代码
console.log(2)
- 接着是 async2 中的同步代码
console.log(4)
然后呢??? 这个await
怎么处理?await
后面的代码又如何处理呢?
其实 async
函数返回一个的是一个 Promise 对象,当函数执行的时候,一旦遇到 await
就会先执行 await
后面的方法,然后将之后的代码全部放入到microtasks
中并继续执行同步任务,所以这里就会在执行完async1
中的 await async2()
之后把后面的console.log(3)
放入到microtasks
。
继续就是:
- 将
console.log(3)
放入到microtasks
中,跳出async1
继续执行同步代码console.log(5)
- 遇到
Promise
,同步执行Promise的构造函数,这里Promise构造函数为空。 - 将
Promise
的then
中的代码放到microtasks
中 - 继续执行同步代码
console.log(7)
- 同步代码执行完成,开始执行
microtasks
中的代码,先后顺序是async1
中的console.log(3)
、Promise.then
中的console.log(6)
所以结果是:
1
2
4
5
7
3
6
好了,相关的概念都解释完了,再让我们回过头来看看那个著名的面试题目吧:
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("promise end");
});
console.log("script end");
开战!
- 首先执行同步代码
console.log("script start")
- 遇到
setTimeout
放到macrotasks
宏任务中 - 继续执行同步代码
async1
中的console.log("async1 start")
- 同步执行
async2
中的console.log("async2");
,并将await
之后的代码统一放入到microtasks
中 - 遇到
Promise
,同步执行其构造函数console.log("promise1");
并将其resmove
之后的then
函数放入到microtasks
中 - 继续同步执行最后一行的
console.log("script end")
,同步任务执行完毕, 检查microtasks
中是否有任务,并执行 - 执行
microtasks
中的第一个任务async1 await
之后的内容console.log("async1 end");
- 执行
microtasks
下一个任务,Promise resolve
中的console.log("promise end");
microtasks
执行完毕,检查macrotasks
中是否有任务- 执行
macrotasks
中的setTimeout
回调console.log("setTimeout")
所以结果是:
script start
async1 start
async2
promise1
script end
async1 end
promise end
undefined
setTimeout
以上,你学废了么?
参考链接:
常见的microtasks有:
常见的macrotasks有:
- setTimeout
- setInterval
- setImmediate
- requestAnimationFrame
- I/O
- UI rendering