「过程详解」async await综合题
wuwhswuwhs -前言
如果你之前跟我一样一直对async
await
熟悉又陌生的话(熟悉是可能每天都在用,陌生是针对一些组合题又丈二和尚摸不着头脑),不妨可以边看边练,总结规律,相信会逐渐清晰并有所得。本文对每个案例都详细描述了代码的执行流程,如有不妥欢迎指正。
async
函数默认会返回一个Promise
对象,不管最后函数有没有return
值。但是针对具体的返回值情况,实际上表现会有所不同,下面分别看看。
这里的普通值是指基础类型值(Number
、String
和Boolean
等)和非thenable
和非Promise
的值
async function foo() {
return 'foo'
}
foo().then(() => console.log('a'))
Promise.resolve()
.then(() => console.log('b'))
.then(() => console.log('c'))
// 输出结果:a b c
很简单,不出意外输出a b c
,也就是async
函数在执行完成后是没有等待的。
foo()
执行完成没有等待,遇到then
将console.log('a')
放入微任务队列;继续往下执行Promise.resolve()
,遇到then
将console.log('b')
入队,当前同步任务全部执行完成;开始执行微任务队列,首先取出并执行console.log('a')
输出a
;然后取出并执行console.log('b')
输出b
,此时遇到then
将console.log('c')
入队;最后取出并执行console.log('c')
输出c
,至此微任务队列清空,代码执行结束;
所谓值为thenable
是指定义了then
方法的对象,可以是一个字面对象,也可以是一个Class
实例。
class Bar {
then(resolve) {
resolve()
console.log('then')
}
}
async function foo() {
// return new Bar()
return {
then(resolve, reject) {
resolve()
console.log('then')
}
}
}
foo().then(() => console.log('a'))
Promise.resolve()
.then(() => console.log('b'))
.then(() => console.log('c'))
// 输出结果:then b a c
怎么顺序不一样了呢?
如果async
函数的返回值是一个thenable
,等同于生成一个Promise
,在foo
函数执行完成,并且Promise
状态变更(resolve
或者reject
)后,还要等1个then
的时长foo()
返回thenable
值,执行then
方法,Promise
状态变更,执行console.log('then')
输出then
,等待1个then
时长;继续往下执行Promise.resolve()
,遇到then
将console.log('b')
放入微任务队列,当前同步任务执行完成;开始执行微任务队列,首先取出并执行console.log('b')
输出b
,当前微任务队列清空;此时步骤1等待时长到期,遇到then
将console.log('a')
放入队列,取出执行输出a
;继续步骤3遇到then
将console.log('c')
放入队列,取出执行输出c
,至此微任务队列清空,代码执行结束;这里如果foo
函数返回的thenable
方法的状态没有变更,则后面的foo().then
将永远不会执行。
async function foo() {
return {
then(resolve, reject) {
console.log('then')
}
}
}
foo().then(() => console.log('a'))
Promise.resolve()
.then(() => console.log('b'))
.then(() => console.log('c'))
// 输出结果:then b c
return 值为Promisereturn
后面的值是Promise
,比如 new Promise(resolve=>resolve())
和Promise.resolve
。
async function foo() {
return Promise.resolve('foo')
}
foo().then(() => console.log('a'))
Promise.resolve()
.then(() => console.log('b'))
.then(() => console.log('c'))
.then(() => console.log('d'))
// 输出结果:b c a d
明显可以看出async
函数执行完后延迟了2个then
时长。
foo()
返回Promise
值,Promise
状态变更,等待2个then
时长;继续往下执行Promise.resolve()
,遇到then
将console.log('b')
放入微任务队列,当前同步任务执行完成;开始执行微任务队列,首先取出并执行console.log('b')
输出b
,当前微任务队列清空;遇到then
将console.log('c')
放入队列,取出执行输出c
;此时步骤1等待时长到期,遇到then
将console.log('a')
放入队列,取出执行输出a
;继续步骤4遇到then
将console.log('d')
放入队列,取出执行输出d
,至此微任务队列清空,代码执行结束;综合上述表现可以总结出如下规律
既然async
函数返回值对代码执行顺序有影响,那么await
后面的表达式值是否也有影响呢?下面同样分为上述三种场景进行实验分析
async function foo() {
await 'foo'
console.log('a')
}
foo().then(() => console.log('b'))
Promise.resolve()
.then(() => console.log('c'))
.then(() => console.log('d'))
// 输出结果:a c b d
可以判断,await
后面的表达式值如果是普通值,无须等待then
时长。那么,为什么b
会在c
后面输出呢?
await
表达式有执行结果后,await
下一行到函数结束部分代码codex
可以看做放置到微任务队列中,等同于Promise.resolve(await xxx).then(()=>codex)
,这里是伪代码,await
在时间顺序上等效于Promise.prototype.then
。await 'foo'
执行完成后,console.log('a')
被添加到微任务队列;继续往下执行同步任务Promise.resolve()
,遇到then
将console.log(c)
添加到微任务队列,当前同步任务执行完成;然后执行微任务队列中任务,取出并执行console.log('a')
输出a
;此时foo
函数执行完成,遇到then
将console.log('b')
入队;继续执行微任务队列中console.log('c')
输出c
,此时遇到then
将console.log('d')
入队;最后依次执行取出剩余微任务,执行并输出b
和d
,至此微任务队列清空,代码执行结束;
async function foo() {
await {
then(resolve) {
resolve()
console.log('then')
}
}
console.log('a')
}
foo().then(() => console.log('b'))
Promise.resolve()
.then(() => console.log('c'))
.then(() => console.log('d'))
.then(() => console.log('e'))
// 输出结果 then c a d b e
await
后面表达式值如果是thenable
,需要等待1个then
时长,才会去执行后续代码。
foo()
执行await
是一个thenable
,Promise
状态变更,执行同步代码console.log('then')
,输出then
,此时等待1个then
时长;继续往下执行同步任务Promise.resolve()
,遇到then
将console.log('c')
加入到微任务队列,当前同步任务执行完成;开始执行微任务队列,取出并执行console.log('c')
,输出c
,微任务队列清空;此时步骤1等待时长到期,将await
后续代码console.log('a')
入队;继续步骤3,遇到then
将console.log('d')
入队,然后依次取出console.log('a')
和console.log('d')
并执行,输出a
和d
;执行完console.log('d')
遇到then
将console.log('e')
放入队列,取出执行,输出e
;确实有点绕,我们将1个then
等待时长看做是下一个微任务从入队到执行完成出队的时间就好。比如这里c
任务执行完成,下一个任务d
正准备进入被a
插了队。
async function foo() {
await Promise.resolve('foo')
console.log('a')
}
foo().then(() => console.log('b'))
Promise.resolve()
.then(() => console.log('c'))
.then(() => console.log('d'))
.then(() => console.log('e'))
// 输出结果 a c b d e
await
后面表达式如果是Promise
,和普通值的结果是一样,无须等待then
时长。
为什么不和return
为Promise
的情景一样是2次呢?原来这是nodejs
在后期版本优化后的结果:移除了2个微任务,1个throwaway promise
,具体原因可以查看「译」更快的 async 函数和 promises。
。
对于早期版本(node 11
及以前),输出的结果是c d a e b
,需要等待2个then
等待时长。
foo()
执行await
是一个Promise
,Promise
状态变更,此时等待2个then
时长;继续往下执行同步任务Promise.resolve()
,遇到then
将console.log('c')
加入到微任务队列,当前同步任务执行完成;开始执行微任务队列,取出并执行console.log('c')
,输出c
,微任务队列清空;遇到then
将console.log('d')
入队,去除并执行,输出d
,微任务队列清空;此时步骤1等待时长到期,将await
后续代码console.log('a')
入队;继续步骤4,遇到then
将console.log('e')
入队,然后依次取出console.log('a')
和console.log('e')
并执行,输出a
和e
;执行完console.log('a')
遇到then
将console.log('b')
放入队列,取出执行,输出b
;综合await
表达式值的结果,我们可以总结
以上我们仅仅从async
的return
值和await
表达式值单一视角来看,下面综合他们两个来分析(统一在node 12+
环境)。
首先,await是一个普通函数(非async
函数)
function baz() {
// console.log('baz')
// return 'baz'
// return {
// then(resolve) {
// console.log('baz')
// resolve()
// }
// }
return new Promise((resolve) => {
console.log('baz')
resolve()
})
}
async function foo() {
await baz()
console.log('a')
}
foo().then(() => console.log('b'))
Promise.resolve()
.then(() => console.log('c'))
.then(() => console.log('d'))
.then(() => console.log('e'))
// await baz函数return是普通值 输出结果是baz a c b d e
// await baz函数return是thenable 输出结果是 baz c a d b e
// await baz函数return是Promise 输出结果 baz a c b d e
与直接await
表达式值输出一致。
return
是普通值,不等待then
时长;baz函数return
是thenable
,等待1个then
时长;baz函数return
是Promise
,不等待then
时长;
然后将baz
函数改成async
async function baz() {
// console.log('baz')
// return 'baz'
// return {
// then(resolve) {
// console.log('baz')
// resolve()
// }
// }
return new Promise((resolve) => {
console.log('baz')
resolve()
})
}
async function foo() {
await baz()
console.log('a')
}
foo().then(() => console.log('b'))
Promise.resolve()
.then(() => console.log('c'))
.then(() => console.log('d'))
.then(() => console.log('e'))
// await baz函数return是普通值 输出结果是baz a c b d e
// await baz函数return是thenable 输出结果是 baz c a d b e
// await baz函数return是Promise 输出结果 baz c d a e b
// node12 以下版本 await baz函数return是Promise 输出结果 baz c d e a b
从中我们可以发现:await
async
函数的等待时长与async baz
函数的return
值等待时长保持一致。
return
是普通值,不等待then
时长;async baz函数return
是thenable
,等待1个then
时长;async baz函数return
是Promise
,等待2个then
时长,但是在node12
以下版本会等待3个then
时长;综合async、await、Promise、then和setTimeout下面我们综合async
、await
、Promise
、then
和setTimeout
来看一道题目
const async1 = async () => {
console.log('async1')
setTimeout(() => {
console.log('timer1')
}, 2000)
await new Promise((resolve) => {
console.log('promise1')
resolve()
})
console.log('async1 end')
return Promise.resolve('async1 success')
}
console.log('script start')
async1().then((res) => console.log(res))
console.log('script end')
Promise.resolve(1)
.then(Promise.resolve(2))
.catch(3)
.then((res) => console.log(res))
setTimeout(() => {
console.log('timer2')
}, 1000)
思考几分钟,输出结果
// script start
// async1
// promise1
// script end
// async1 end
// 1
// async1 success
// timer2
// timer1
执行同步任务输出script start
和async1
,遇到setTimeout
放入宏任务队列;继续往下执行await
表达式,执行new Promise
输出promise1
,Promise
状态变更,不等待then
时长,将后续代码添加到微任务队列;继续往下执行输出script end
,执行Promise.resolve(1)
遇到then
将Promise.resolve(2)
放入微任务队列;再往下执行遇到setTimeout
放入宏任务队列,至此同步任务执行完毕;开始执行微任务队列,取出并执行步骤2的后续代码输出async1 end
,返回一个已变更的Promise
对象,需要等待2个then
时长;继续取出微任务Promise.resolve(2)
并执行,状态为resolved
后面走then
;遇到then
将(res) => console.log(res)
放入微任务队列,然后取出并执行输出1
,注意:then
中是非函数表达式会执行,默认返回的是上一个Promise
的值,then(Promise.resolve(2))
会透传上一层的1
;此时步骤5等待时长到期,将(res) => console.log(res)
放入微任务队列,然后取出并执行输出async1 success
;最后2个定时器分别到期,输出timer2
和timer1
;如果对这个案例再稍作改造
const async1 = async () => {
console.log('async1')
setTimeout(() => {
console.log('timer1')
}, 2000)
await new Promise((resolve) => {
console.log('promise1')
})
console.log('async1 end')
return 'async1 success'
}
console.log('script start')
async1().then((res) => console.log(res))
console.log('script end')
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.catch(4)
.then((res) => console.log(res))
setTimeout(() => {
console.log('timer2')
}, 1000)
// 输出结果:
// script start
// async1
// promise1
// script end
// 1
// timer2
// timer1
具体过程就不一一列举了,从输出结果可以发现:如果await
表达式的Promise
的状态没有变更,以下代码以及后面的then
永远都不会执行。then
的执行时机是在前面函数执行完成并且Promise
状态变更以后才会被添加到微任务队列中等待执行。
通过以上就是基本async
await
的使用场景,以及综合then
、Promise
和setTimeout
的混合使用,大致可以总结如下几条规律:
async
函数的return
值为thenable
会等待1个then
时长,值为Promise
会等待2个时长;await
表达式值为thenable
会等待1个then
时长,值为Promise
在node12+
不等待then
时长,低版本node
等待2个then
时长;await
一个async
函数,async
函数的return
值为thenable
会等待1个then
时长,值为Promise
在node12+
会等待2个then
时长,在低版本node
等待3个then
时长;如果then
中是非函数,表达式本身会执行,默认返回的是上一个Promise
的值,也就是透传上一个Promise
结果;如果await
表达式的Promise
的状态没有变更,以下代码以及后面的then
永远都不会执行;以上案例均通过实验运行得出,流程如有解释错误,欢迎指正,完~