从一道面试题解读Promise/async/await执行顺序

2,639 阅读9分钟

面试题如下:

console.log('script start')

async function async1() {
    await async2()
    console.log('async1 end')
}
async function async2() {
    console.log('async2 end')
    return Promise.resolve().then(()=>{
        console.log('async2 end1')
    })
}
async1()

setTimeout(function() {
    console.log('setTimeout')
})

new Promise(resolve => {
    console.log('Promise')
    resolve()
})
.then(function() {
    console.log('promise1')
})
.then(function() {
    console.log('promise2')
})
.then(function() {
    console.log('promise3')
})
Promise.resolve().then(function() {
    console.log('promise4')
})

console.log('script end')

先打印输出:

Chrome环境:版本 89.0.4389.82(正式版本) (x86_64) 旧版本存在差异
eventloop.js:134 script start
eventloop.js:153 async2 end
eventloop.js:165 Promise
eventloop.js:181 script end
eventloop.js:155 async2 end1
eventloop.js:169 promise1
eventloop.js:178 promise4
eventloop.js:172 promise2
eventloop.js:143 async1 end
eventloop.js:175 promise3
eventloop.js:161 setTimeout

Node环境:v15.3.0
script start
async2 end
Promise
script end
async2 end1
promise1
promise4
promise2
async1 end
promise3
setTimeout

这道题涉及到的一些知识点:

a、EventLoop

zhuanlan.zhihu.com/p/33058983

b、Promise

tc39.es/ecma262/#se… tc39.es/ecma262/#se…

c、async/await

tc39.es/ecma262/#se…

主要内容:

1Promise的链式调用then函数是怎么执行的;
2new Promise(r=>r())和Promise.resolve()分析;
3async标识函数的特性;
4await特性;

Promise的链式调用then函数是怎么执行的:

new Promise(resolve=>resolve())
.then(()=>{
	console.log(1)
})
.then(()=>{
	console.log(2)
})
.then(()=>{
	console.log(3)
})
new Promise(resolve=>resolve())
.then(()=>{
	console.log(4)
})
.then(()=>{
	console.log(5)
})
.then(()=>{
	console.log(6)
})
/**
输出:
1
4
2
5
3
6
*/

解析上面代码输出:

1、第一个Promise构造器的执行器函数(new Promise)内部的同步代码执行到 resolve(),此时状态变为_Promise {< fulfilled >: undefined}_,接下来将紧随的then的回调函数压入微任务队列中等待执行

此时的微任务队列 [ console.log(1) ]

2、接着继续执行,遇到第二个Promise构造器的执行器函数(new Promise)内部的同步代码执行到 resolve(),同第一个,此时将紧随的then中的回调函数添加到微任务队列中等待执行

此时的微任务队列 [ console.log(1) -> console.log(4) ]

3、全部同步任务代码执行完毕,开始执行微任务队列中任务,取队列第一个任务执行,打印 '1' ,将紧随then中回调函数添加到微任务队列中;( console.log(1) 出队列 )

取【console.log(1)】执行 输出 1console.log(1)】出队列【console.log(2)】入队列
[ console.log(4) -> console.log(2) ]
// 终端打印// 1

4、接下来就类似第3步;

取【console.log(4)】执行 输出 4console.log(4)】出队列【console.log(5)】入队列
[ console.log(2) -> console.log(5) ]
// 终端打印// 1 4

取【console.log(2)】执行 输出 2console.log(2)】出队列 【console.log(3)】入队列
[ console.log(5) -> console.log(3) ]
// 终端打印// 1 4 2

取【console.log(5)】执行 输出 5console.log(5)】出队列【console.log(6)】入队列
[ console.log(3) -> console.log(6) ]
// 终端打印// 1 4 2 5 

取【console.log(3)】执行 输出 3console.log(3)】出队列
[ console.log(6) ]
// 终端打印// 1 4 2 5 3 

取【console.log(6)】执行 输出 6console.log(6)】出队列
[ ]
// 终端打印// 1 4 2 5 3 6

总结:

  • Promise.prototype.then 会隐式返回一个新的Promise;
  • Promise的then回调是否会被推入微任务队列取决于Promise的状态是否是 fulfilled 或 rejected
    • a.Promise的状态是pending时,调用then会在该Promise上注册一个回调,等待Promise状态变更;
    • b.Promise的状态是fulfilled或rejected时,调用then会立即创建一个微任务,将注册的回调推入微任务队列中;

new Promise(r=>r())和Promise.resolve()分析

let v = new Promise(resolve => {
    console.log("v-begin");
    resolve("v-then");
});
// 1、new Promise(resolve => resolve(v))
new Promise(resolve => resolve(v))
// 2、Promise.resolve(v)
// Promise.resolve(v)
.then((v) => {
    console.log(v)
});
new Promise(resolve => {
    console.log(1);
    resolve();
})
.then(() => {
    console.log(2);
})
.then(() => {
    console.log(3);
})
.then(() => {
    console.log(4);
});
/**
// 1、使用new Promise(resolve => resolve(v))输出
v-begin
1
2
3
v-then
4
按照输出可以看出微任务队列如下:
[ console.log(2) -> console.log(3) -> console.log(v) -> console.log(4) ]
*/
/**
// 2、使用Promise.resolve(v)输出
v-begin
1
v-then
2
3
4
按照输出可以看出微任务队列如下:
[ console.log(v) -> console.log(2) -> console.log(3) -> console.log(4) ]
*/

可以看出new Promise(r=>r())和Promise.resolve() 在处理上有些不同,那到底各自是怎么去处理的呢?

1、先看Promise.resolve(v)

TC39规范

WX20210318-145725@2x.png 处理v参数简单说有以下几种情况

  • 参数是一个 Promise 实例

    如果参数是一个Promise实例,则不做任何处理,直接返回这个实例

  • 参数是一个thenable对象

    let thenable = {
      then: function(resolve, reject) {
        resolve(1);
      }
    };
    

    Promise.resolve方法会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then方法。

    let v = {
        then: function(resolve, reject) {
            console.log("v-begin");
            resolve("v-then");
        }
    };
    Promise.resolve(v)
    // 等价于
    Promise.resolve().then(()=>{
        console.log("v-begin");
        return "v-then"
    })
    
  • 参数不是具有then方法的对象,或者不是对象,再或者不传参数

    直接返回一个resolved状态的 Promise 对象

注意:立即resolve()的 Promise 对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时。

那么对于Promise.resolve(v)处理的微任务队列入队出队如下:

let v = new Promise(resolve => {
    resolve("v-then");
});
Promise.resolve(v)
.then((v) => {
    console.log(v)
});
new Promise(resolve => {
    resolve();
})
.then(() => {
    console.log(2);
})
.then(() => {
    console.log(3);
})
.then(() => {
    console.log(4);
});
因为v是一个Promise实例,Promise.resolve(v)会原封不动的返回,则:
此时的微任务队列 [ console.log(v) ]
继续执行代码【console.log(2)】入队列
此时的微任务队列 [ console.log(v) -> console.log(2)]

取【console.log(v)】执行 输出 v-then  【console.log(v)】出队列
[ console.log(2) ]
// 终端打印// v-then
取【console.log(2)】执行 输出 2console.log(2)】出队列 【console.log(3)】入队列
[ console.log(3) ]
// 终端打印// v-then 2
取【console.log(3)】执行 输出 3console.log(3)】出队列 【console.log(4)】入队列
[ console.log(4) ]
// 终端打印// v-then 2 3
取【console.log(4)】执行 输出 4console.log(4)】出队列
[  ]
// 终端打印// v-then 2 3 4

2、分析new Promise(r=>r(v))

TC39规范

promise resolve.png NewPromiseResolveThenableJob.png 处理v参数分析有以下两种情况

  • 参数不是具有then方法的对象,或者不是对象,再或者不传参数

    直接返回一个resolved状态的 Promise 对象

  • 参数是具有then方法的对象或直接是一个Promise对象

    通过NewPromiseResolveThenableJob()将v(thenable)包装成Promise 对象,这个过程是一个微任务,会加入微任务队列,等到下次循环发现v(thenable)是一个resolved状态的Promise,然后会执行v(thenable)对象的then方法,回调加入微任务队列等待执行。(不管v是Promise还是非Promise的thenable对象都会进行一次包装)

let v = new Promise(resolve => {
    resolve("v-then");
});
new Promise(resolve=>resolve(v))
.then((v) => {
    console.log(v)
});
new Promise(resolve => resolve())
.then(() => {
    console.log(2);
})
.then(() => {
    console.log(3);
})
.then(() => {
    console.log(4);
});
NewPromiseResolveThenableJob()对v进行包装,此过程是一个微任务,暂设置为【 p 】;
此时微任务队列[ p ]
执行到下面的new Promise(resolve => resolve()) 将【 console.log(2) 】入队列
[ p -> console.log(2) ]
取p执行,【 p 】出队列,发现v是resolved状态,则执行v的then方法,这也是一个微任务,暂设置为【 vt 】
[ console.log(2) -> vt]
取console.log(2)执行,【 console.log(2) 】出队列	【 console.log(3) 】 入队列
[ vt -> console.log(3) ]
// 终端打印// 2
取vt执行,【 vt 】出队列	new Promise(resolve=>resolve(v))执行完毕,返回一个Promise,并执行其then 将【 console.log(v) 】 入队列
[ console.log(3) -> console.log(v) ]
// 终端打印// 2console.log(3)执行,【 console.log(3) 】出队列 【 console.log(4) 】 入队列
[ console.log(v) -> console.log(4) ]
// 终端打印// 2 3console.log(v)执行,【 console.log(v) 】出队列
[ console.log(4) ]
// 终端打印// 2 3 v-thenconsole.log(4)执行,【 console.log(4) 】出队列
[  ]
// 终端打印// 2 3 v-then 4

async标识函数的特性

async 函数是什么?一句话,它就是 Generator 函数的语法糖。 async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

TC39规范

asyncfunctionstart.png

  • async修饰的函数必定返回一个 Promise 对象;
  • async修饰的函数若没有返回值时,Promise的resolve方法会传递一个undefined值;
  • async修饰的函数若有返回值时,Promise的resolve方法会传递这个值;
  • async修饰的函数若抛出异常,Promise的reject方法会传递这个异常值;
async function add(){}
add().then(res=>console.log(res))
// 等价于
// Promise.resolve().then(res=>console.log(res))
// undefined

async function add(){
		return 1 
}
add().then(res=>console.log(res))
// 等价于
// Promise.resolve(1).then(res=>console.log(res))
// 1

async function add(){
    return Promise.resolve().then(res=>console.log(111))
}
add().then(res=>console.log(res))
// 等价于
// Promise.resolve(Promise.resolve().then(res=>console.log(111)))
//  .then(res=>console.log(res)) 
// -> Promise.resolve().then(res=>console.log(111))
//		  .then(res=>console.log(res))
// 111
// undefined

await特性

TC39规范

await.png promiseresolvec.png performpromisethen.png await v

  • await后面的v会被转化成Promise;
  • 即使 v 是一个已经fulfilled的Promise,还是会新建一个Promsie,并在这个新的Promise中resolve(v); 27.2.5.4.1-10
  • await v 之后的代码等于传入then函数的回调 -> 6.2.3.1-11/12 完成新建Promise,恢复执行上下文
// 由上可对await v进行转换
async function async1() {
    await async2()
    console.log('async1 end')
}
等价于
async function async1() {
    new Promise(resolve=>resolve(async2())).then(()=>{
        console.log('async1 end')
    })
}

最后我们对这道面试题进行分析

console.log('script start')

async function async1() {
    // await async2()
    // console.log('async1 end')
  	//1. 进行转换 ->
  	new Promise(resolve=>resolve(async2())).then(()=>{
        console.log('async1 end')
    })
}
// async function async2() {
//     console.log('async2 end')
//     return Promise.resolve().then(()=>{
//         console.log('async2 end1')
//     })
// }
//2. 进行转换 ->
function async2() {
    console.log('async2 end')
    return Promise.resolve().then(()=>{
        console.log('async2 end1')
    })
}
async1()

setTimeout(function() {
    console.log('setTimeout')
})

new Promise(resolve => {
    console.log('Promise')
    resolve()
})
.then(function() {
    console.log('promise1')
})
.then(function() {
    console.log('promise2')
})
.then(function() {
    console.log('promise3')
})
Promise.resolve().then(function() {
    console.log('promise4')
})

console.log('script end')
[  ]:微任务队列 [[  ]]:宏任务队列

首先执行console.log('script start')
// 终端输出 script start

执行async1() -> new Promise(resolve=>resolve(async2())) 

执行async2(),console.log('async2 end') 直接打印, then回调【 console.log('async2 end1') 】入队列
async2()执行完返回一个Promise,此时NewPromiseResolveThenableJob(async2())包装的Promise的then回调加入微任务队列,暂设置为【 p 】
[ console.log('async2 end1') -> p ]
// 终端输出 script start -> async2 end

继续向下执行,setTimeout 【【 console.log('setTimeout') 】】 入队列
[ console.log('async2 end1') -> p ] 
[[ console.log('setTimeout') ]]
// 终端输出 script start -> async2 end

继续向下执行,console.log('Promise') 直接打印,then回调【 console.log('promise1') 】入队列
[ console.log('async2 end1') -> p -> console.log('promise1') ] 
[[ console.log('setTimeout') ]]
// 终端输出 script start -> async2 end -> Promise

继续执行,then回调【 console.log('promise4') 】入队列
[ console.log('async2 end1') -> p -> console.log('promise1') -> console.log('promise4') ] 
[[ console.log('setTimeout') ]]
// 终端输出 script start -> async2 end -> Promise

继续执行,console.log('script end') 直接打印
[ console.log('async2 end1') -> p -> console.log('promise1') -> console.log('promise4') ] 
[[ console.log('setTimeout') ]]
// 终端输出 script start -> async2 end -> Promise -> script end

同步执行完毕,开始取微任务队列任务执行
取console.log('async2 end1'),【 console.log('async2 end1') 】出队列,此时async2()返回的Promise状态才转为resolved
[ p -> console.log('promise1') -> console.log('promise4') ]
[[ console.log('setTimeout') ]]
// 终端输出 script start -> async2 end -> Promise -> script end -> async2 end1

取p执行,【 p 】出队列,发现async2()是resolved状态,则执行async2()的then,其回调是一个微任务,暂设置为【 vt 】, 【 vt 】入队列
[ console.log('promise1') -> console.log('promise4') -> vt ]
[[ console.log('setTimeout') ]]
// 终端输出 script start -> async2 end -> Promise -> script end -> async2 end1console.log('promise1'),【 console.log('promise1') 】出队列 【 console.log('promise2') 】 入队列
[ console.log('promise4') -> vt -> console.log('promise2') ]
[[ console.log('setTimeout') ]]
// 终端输出 script start -> async2 end -> Promise -> script end -> async2 end1 -> promise1console.log('promise4'),【 console.log('promise4') 】出队列
[ vt -> console.log('promise2') ]
[[ console.log('setTimeout') ]]
// 终端输出 script start -> async2 end -> Promise -> script end -> async2 end1 -> promise1 -> promise4

取vt执行,【 vt 】出队列	new Promise(resolve=>resolve(async2()))执行完毕,返回一个Promise,并执行其then 将【 console.log('async1 end') 】 入队列
[ console.log('promise2') -> console.log('async1 end') ]
[[ console.log('setTimeout') ]]
// 终端输出 script start -> async2 end -> Promise -> script end -> async2 end1 -> promise1 -> promise4console.log('promise2'),【 console.log('promise2') 】出队列 【 console.log('promise3') 】 入队列
[ console.log('async1 end') -> console.log('promise3') ]
[[ console.log('setTimeout') ]]
// 终端输出 script start -> async2 end -> Promise -> script end -> async2 end1 -> promise1 -> promise4 -> promise2console.log('async1 end'),【 console.log('async1 end') 】出队列
[ console.log('promise3') ]
[[ console.log('setTimeout') ]]
// 终端输出 script start -> async2 end -> Promise -> script end -> async2 end1 -> promise1 -> promise4 -> promise2 -> async1 endconsole.log('promise3'),【 console.log('promise3') 】出队列
[  ]
[[ console.log('setTimeout') ]]
// 终端输出 script start -> async2 end -> Promise -> script end -> async2 end1 -> promise1 -> promise4 -> promise2 -> async1 end -> promise3console.log('setTimeout'),【【 console.log('setTimeout') 】】出队列
[  ]
[[  ]]
// 终端输出 script start -> async2 end -> Promise -> script end -> async2 end1 -> promise1 -> promise4 -> promise2 -> async1 end -> promise3 -> setTimeout

至此整题解析完毕。