最近做了一道程序输出题,发现以前做过然后现在又忘记了,就想记录下来,希望能更加地浅显易懂地表达它是如何得出它的结果的。
我们先来看一段代码,结果会输出什么?
1 async function async1() {
2 console.log('async1 start')
3 await async2()
4 console.log('async1 end') // 1
5 }
6
7 async function async2() {
8 console.log('async2')
9 }
10
11 console.log('script start')
12
13 async1()
14
15 setTimeout(() => { // 2
16 console.log('setTimeout')
17 }, 0)
18
19 new Promise((resolve) => {
20 console.log('promise')
21 resolve()
23 }).then(function () { // 3
24 console.log('promise1 start')
25 return 'promise1 end'
26 }).then(res => { // 4
27 console.log(res)
28 }).then(res => { // 5
29 console.log(res)
30 })
31
32 console.log('script end')
题中有async/await、promise、setTimeout,很明显是想要考微任务、宏任务的执行顺序。 我们从上往下看,先是两个async函数的声明
async是ES7才有的与异步操作有关的关键字,async函数返回一个Promise对象,可以使用then方法添加回调函数,即
async fn1() {}
console.log(fn1()) // 打印 一个Promise对象
然后,11行打印 script start
13行执行函数async1,打印 async1 start
; 遇到await操作符
await操作符用于等待一个Promise对象,它只能在异步函数async function内部函数使用
[return_value] = await expression // expression---一个Promise对象 或 任何需要等待的值
需要注意的是,await语句之后的代码,相当于Promise的callback,是微任务 即
await async2()
相当于
await new Promise(resolve => {
console.log('async2')
resolve()
}).then(() => { // 微任务1
console.log('async1 end')
})
async标记的函数返回一个Promise对象,因此,我们又可以看成
await async2().then(() => { // 微任务1
console.log('async1 end')
})
因此,题中遇到await操作符,执行async2函数,打印 async2
,之后的代码作为一个微任务加入到微任务队列中,等待同步代码, 即调用栈中任务执行完毕后执行。
此时,微任务队列为[1] (为题中右边标序)
继续向后走,遇到setTimeout为宏任务,加入到宏任务队列中
此时,宏任务队列为[2]
遇到new Promise直接执行,打印 promise
, 遇到then,将回调函数放入微任务队列中,微任务队列为[1, 3],继续遇到then,加入到微任务队列中,微任务队列为[1, 3, 4],又遇到then,继续加入微任务队列,此时,微任务队列为[1, 3, 4, 5]
这里删除了之前的说法,是因为在后面的变形中,我理解到这样的说法是错误的,then加入微任务队列的时机,并不是像我们臆想的那样顺序连续地加入,而是
补充:
(1)第一个then回调是在Promise对象的resolve后加入微任务队列的
(2)后续的then回调都依赖于上一个then回调代码的执行结束,同时关于前一个then的同步或异步,其他博主也有讲到:①上一个then回调代码都是同步执行,执行结束后下一个then可以加入到微任务队列中;
②上一个then回调有return关键字时,需要等待return的内容完全执行完毕后,下一个then才能加入到微任务队列中
因此解题思路应为:resolve后遇到第一个then,我们将其放入微任务队列中,微任务队列为[1, 3]
继续执行程序,打印 script end
,此时程序中的同步代码已经执行完毕,调用栈为空。
开始将微任务队列的任务逐个加入调用栈中执行,当前微任务队列为[1, 3],因此先执行标识1处的微任务,打印 async1 end
,微任务队列变为[3];
执行标识3处微任务,打印 promise1 start
,微任务队列变为空;
至此,第一个then执行完毕,第二个then加入到微任务队列中,为[4],执行标识4处微任务,打印promise1 end
,接着第三个then加入到微任务队列中,为[5],打印undefined
。
第二个then因为有上一个then通过return传递的'promise1 end',而第二个then没有传递参数给予到下一个then,因此第三个then打印的是undefined。
此时微任务队列为空,开始执行宏任务队列中的任务,只有一个标识2处的宏任务,打印 setTimeout
至此,程序执行完毕。 最后结果为
变形
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end') // 1
new Promise(resolve => {
console.log('async promise')
resolve('async promise start')
}).then(res => { // 2
console.log(res)
})
}
async function async2() {
console.log('async2')
}
async1()
new Promise((resolve) => {
console.log('promise')
resolve()
}).then(function () { // 3
console.log('promise1 start')
return 'promise1 end'
}).then(res => { // 4
console.log(res)
}).then(res => { // 5
console.log(res)
})
我们将代码聚焦到重要的部分,并在原来的基础上进行扩展,此时程序又会输出什么呢?我们先来看一下输出的结果
前五个输出我们可以通过上一个例子同理得出,从第六个开始输出顺序就需要第一个例子中对于then进入微任务的时机的补充来说明原因了。
async1函数中,await后的代码是微任务,我们可以将
await async2()
console.log('async1 end')
new Promise(resolve => {
console.log('async promise')
resolve('async promise start')
}).then(res => {
console.log(res)
})
看成
await new Promise(resolve1 => {
console.log('async2')
resolve1()
}).then(() => { // 1
console.log('async1 end')
new Promise(resolve => {
console.log('async promise')
resolve('async promise start')
}).then(res => { // 2
console.log(res)
})
})
await async2()执行后,输出async2
,await后的代码被进入微任务队列中,微任务队列[1],接着执行同步代码,输出promise
,与此同时,标识3处微任务进入队列,微任务队列[1,3]。
同步代码执行完毕,调用栈为空,微任务队列逐个进入调用栈中执行,执行微任务1,执行标识1处then中回调函数,顺序输出async1 end
async promise
,标识2处微任务进入队列,微任务队列[3,2]。
执行微任务3,输出promise1 start
,微任务4进入队列,微任务队列[2,4]。接着执行微任务2,以此类推,依次输出async promise start
promise1 end
undefined
。
至此,对于微任务宏任务,async/await,promise/then的解说就到此结束啦,欢迎探讨!