【JS】async await 和 promise微任务执行顺序问题
问题描述
今天看到一个关于js执行顺序的问题,不太了解async await中await后的代码的执行时机
- 问题1. 为啥promise2、promise3输出比async1 end输出早?如果都是微任务的话,不是async1 end先加入微任务队列的吗?
- 问题2. 为什么async1 end又先于promise4输出呢?
相关代码
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')
}).then(function() {
console.log('promise3')
}).then(function() {
console.log('promise4')
}).then(function() {
console.log('promise5')
}).then(function() {
console.log('promise6')
}).then(function() {
console.log('promise7')
}).then(function() {
console.log('promise8')
})
console.log('script end')
chrome 70.0.3538.102 结果
script startasync1 start
async2
promise1
script end
promise2 // 与 chrome canary 73 不一致
promise3 // 与 chrome canary 73 不一致
async1 end // 与 chrome canary 73 不一致
promise4
promise5
promise6
promise7
promise8
setTimeout
Chrome canary 73.0.3646.0(同node8.12.0):
script startasync1 start
async2
promise1
script end
async1 end // 与 chrome 70 不一致
promise2 // 与 chrome 70 不一致
promise3 // 与 chrome 70 不一致
promise4
promise5
promise6
promise7
promise8
setTimeout
回答
基础知识
在你看答案之前,我希望你至少了解
promise
的 executor(执行器) 里的代码是同步的promise
的回调是 microTask(微任务) 而setTimeout
的回调是 task(任务/宏任务)- microTask 早于 task 被执行。
精简题目
我把这道题精简下,先把同步的代码和 setTimeout
的代码删掉,再来解释(期间 await
的部分规范有变动)。
再精简下,问题就是这样:
async function async1(){ await async2()
console.log('async1 end')
}
async function async2(){}
async1();
new Promise(function(resolve){
resolve();
}).then(function(){
console.log('promise2')
}).then(function() {
console.log('promise3')
}).then(function() {
console.log('promise4')
})
为什么在 chrome canary 73 返回
async1 endpromise2
promise3
promise4
而在 chrome 70 上返回
promise2promise3
async1 end
promise4
正文
我对 promise
稍微熟悉些,其实也不熟,但是把 await
转成 promise
会相对好理解些,不知道有没有同感?
这道题其实问的是
await async2()
怎么理解?
因为 async
函数总是返回一个 promise
,所以其实就是在问
await promise
怎么理解?
那么我们看下规范 Await
根据提示
async function async1(){ await async2()
console.log('async1 end')
}
等价于
async function async1() { return new Promise(resolve => {
resolve(async2())
}).then(() => {
console.log('async1 end')
})
}
与@Jialiang_T同学给出的一致,但是到这里,仍然不太好理解,
因为RESOLVE(async2())
并不等于Promise.resolve(async2())
为了行文方便,这里开始我们用 RESOLVE
来表示 Promise
构造器里的 resolve
,例如:
new Promise(resolve=>{ resolve()
})
之所以这样,因为 async2()
返回一个 promise
, 是一个 thenable
对象,RESOLVE(thenable)
并不等于 Promise.resolve(thenable)
,而 RESOLVE(non-thenable)
等价于 Promise.resolve(non-thenable)
,具体对照规范的解释请戳
What's the difference between resolve(promise) and resolve('non-thenable-object')?
结论就是:RESOLVE(thenable)
和 Promise.resolve(thenable)
的转换关系是这样的,
new Promise(resolve=>{ resolve(thenable)
})
会被转换成
new Promise(resolve => { Promise.resolve().then(() => {
thenable.then(resolve)
})
})
那么对于 RESOLVE(async2())
,我们可以根据规范转换成:
Promise.resolve().then(() => { async2().then(resolve)
})
所以 async1
就变成了这样:
async function async1() { return new Promise(resolve => {
Promise.resolve().then(() => {
async2().then(resolve)
})
}).then(() => {
console.log('async1 end')
})
}
同样,因为 RESOLVE()
就等价于 Promise.resolve()
,所以
new Promise(function(resolve){ resolve();
})
等价于
Promise.resolve()
所以,题目
async function async1(){ await async2()
console.log('async1 end')
}
async function async2(){}
async1();
new Promise(function(resolve){
resolve();
}).then(function(){
console.log('promise2')
}).then(function() {
console.log('promise3')
}).then(function() {
console.log('promise4')
})
就等价于
async function async1 () { return new Promise(resolve => {
Promise.resolve().then(() => {
async2().then(resolve)
})
}).then(() => {
console.log('async1 end')
})
}
async function async2 () {}
async1()
Promise.resolve()
.then(function () {
console.log('promise2')
})
.then(function () {
console.log('promise3')
})
.then(function () {
console.log('promise4')
})
这就是根据当前规范解释的结果, chrome 70 和 chrome canary 73 上得到的都是一样的。
promise2promise3
async1 end
promise4
Await 规范的更新
那么为什么,chrome 73 现在得到的结果不一样了呢?修改的决议在这里,目前是这个状态。
就像你所看到的一样,为什么要把 async1
async function async1(){ await async2()
console.log('async1 end')
}
转换成
async function async1() { return new Promise(resolve => {
Promise.resolve().then(() => {
async2().then(resolve)
})
}).then(() => {
console.log('async1 end')
})
}
而不是直接
async function async1 () { async2().then(() => {
console.log('async1 end')
})
}
这样是不是更简单直接,容易理解,且提高性能了呢?如果要这样的话,也就是说,
async function async1(){ await async2()
console.log('async1 end')
}
async1
不采用 new Promise
来包装,也就是不走下面这条路:
async function async1() { return new Promise(resolve => {
resolve(async2())
}).then(() => {
console.log('async1 end')
})
}
而是直接采用 Promise.resolve()
来包装,也就是
async function async1() { Promise.resolve(async2()).then(() => {
console.log('async1 end')
})
}
又因为 async2()
返回一个 promise
, 根据规范Promise.resolve,
所以 Promise.resolve(promise)
返回 promise
, 即Promise.resolve(async2())
等价于 async2()
,所以最终得到了代码
async function async1 () { async2().then(() => {
console.log('async1 end')
})
}
这就是贺老师在知乎里所说的
根据 TC39 最近决议,await将直接使用Promise.resolve()相同语义。
tc39 的 spec 的更改体现在
chrome canary 73 采用了这种实现,所以题目
async function async1(){ await async2()
console.log('async1 end')
}
async function async2(){}
async1();
new Promise(function(resolve){
resolve();
}).then(function(){
console.log('promise2')
}).then(function() {
console.log('promise3')
}).then(function() {
console.log('promise4')
})
在 chrome canary 73及未来可能被解析为
async function async1 () { async2().then(() => {
console.log('async1 end')
})
}
async function async2 () {}
async1()
new Promise(function (resolve) {
resolve()
})
.then(function () {
console.log('promise2')
})
.then(function () {
console.log('promise3')
})
.then(function () {
console.log('promise4')
})
//async1 end
//promise2
//promise3
//promise4
在 chrome 70 被解析为,
async function async1 () { return new Promise(resolve => {
Promise.resolve().then(() => {
async2().then(resolve)
})
}).then(() => {
console.log('async1 end')
})
}
async function async2 () {}
async1()
Promise.resolve()
.then(function () {
console.log('promise2')
})
.then(function () {
console.log('promise3')
})
.then(function () {
console.log('promise4')
})
//promise2
//promise3
//async1 end
//promise4
转换后的代码,你应该能够看得懂了,如果看不懂,说明你需要补一补 promise 的课了。
2018.12.26
- 如有错误,欢迎指正,
- 最后,感谢 @Jialiang_T 提醒了我对
resolve(thenable)
和Promise.resolve(thenable)
的思考,也就是 SO 的那个问题。 - 我博客里加了一篇 async, promise order 内容与这个重复较多,思路稍微清晰些,另外多加了一部分对于
async
的转换分析,不过是英文的,自己斟酌要不要去看看,如果觉得不错的话,留言,我可以再翻译成中文。
说一下我个人的理解,如有错误还望指正。
这个问题涉及以下3点:
- async 函数的返回值
- Promise 链式 then() 的执行时机
- async 函数中的 await 操作符到底做了什么
下面一一回答:
async 函数的返回值:
- 被 async 操作符修饰的函数必然返回一个 Promise 对象
- 当 async 函数返回一个值时,Promise 的 resolve 方法负责传递这个值
- 当 async 函数抛出异常时,Promise 的 reject 方法会传递这个异常值
所以,以示例代码中 async2 为例,其等价于
function async2(){
console.log('async2');
return Promise.resolve();
}
Promise 链式 then() 的执行时机
- 多个 then() 链式调用,并不是连续的创建了多个微任务并推入微任务队列,因为 then() 的返回值必然是一个 Promise,而后续的 then() 是上一步 then() 返回的 Promise 的回调
以示例代码为例:
...
new Promise(function(resolve){
console.log('promise1')
resolve();
}).then(function(){
console.log('promise2')
}).then(function() {
console.log('promise3')
})
...
- Promise 构造器内部的同步代码执行到
resolve()
,Promise 的状态改变为 fulfillment, then 中传入的回调函数console.log('promise2')
作为一个微任务推入微任务队列 而第二个 then 中传入的回调函数
console.log('promise3')
还没有被推入微任务队列,只有上一个 then 中的console.log('promise2')
执行完毕后,console.log('promise3')
才会被推入微任务队列,这是一个关键点
- Promise 构造器内部的同步代码执行到
async 函数中的 await 操作符到底做了什么
按照规范,我们可以做个转化:
async function async1(){
console.log('async1 start')
await async2()
console.log('async1 end')
}
可以转化为:
function async1(){
console.log('async1 start')
return RESOLVE(async2())
.then(() => { console.log('async1 end') });
}
问题关键就出在这个
RESOLVE
上了,要引用以下知乎上贺师俊大佬的回答:RESOLVE(p)
接近于Promise.resolve(p)
,不过有微妙而重要的区别:p 如果本身已经是 Promise 实例,Promise.resolve 会直接返回 p 而不是产生一个新 promise;- 如果
RESOLVE(p)
严格按照标准,应该产生一个新的 promise,尽管该 promise 确定会 resolve 为 p,但这个过程本身是异步的,也就是现在进入 job 队列的是新 promise 的 resolve 过程,所以该 promise 的 then 不会被立即调用,而要等到当前 job 队列执行到前述 resolve 过程才会被调用,然后其回调(也就是继续 await 之后的语句)才加入 job 队列,所以时序上就晚了
所以上述的 async1 函数我们可以进一步转换一下:
function async1(){
console.log('async1 start')
return new Promise(resolve => resolve(async2()))
.then(() => {
console.log('async1 end')
});
}
说到最后,最终示例代码近似等价于以下的代码:
function async1(){ console.log('async1 start')
return new Promise(resolve => resolve(async2()))
.then(() => {
console.log('async1 end')
});
}
function async2(){
console.log('async2');
return Promise.resolve();
}
console.log('script start')
setTimeout(function(){
console.log('setTimeout')
},0)
async1();
new Promise(function(resolve){
console.log('promise1')
resolve();
}).then(function(){
console.log('promise2')
}).then(function() {
console.log('promise3')
}).then(function() {
console.log('promise4')
}).then(function() {
console.log('promise5')
}).then(function() {
console.log('promise6')
}).then(function() {
console.log('promise7')
}).then(function() {
console.log('promise8')
})
console.log('script end')
看了最后转换的代码,你应该明白了吧。现在你可以把它粘贴到最新版本的chrome中试试啦!
最后说个题外话,关于RESOLVE(p)
的实现,在旧版本的V8中是不一样的(进行了激进优化,可以简单理解为没有按照规范返回一个新 Promise),所以最终的运行结果也不一致
参考:
关于浏览器中的执行顺序,在目前最新版的chromeV71中会有点问题。会在下个版本中改进,你也可以用自己的工程跑一下,然后用babel的stage-3编译一把,以babel的编译结果为准吧。
具体可以参考,我写的这篇文章。其中也遇到了时序问题。
JS的时间模块也是异步的,如果说先后的话应该是settimeout先加入异步队列,awite会先等待awite之后的函数执行完毕之后再执行
你换node环境执行下
标准应该没规定,我当时自己测的chrome和firefox执行顺序不一样的
刚刚测了一下,edge和chrome表现不一样的,反正这个微任务执行顺序标准没规定,具体表现要看浏览器自己怎么处理了
现在有标准了,以后所有浏览器应该有一致的表现(说实话,都是异步,除了js引擎开发者,关注谁先谁后意义没什么意义)
我的理解是,reslove调用后,会把promise2加入到任务队列,然后执行一次Event Loop
reslove调用后,会把promise2加入到任务队列,然后执行一次Event Loop
以上是 【JS】async await 和 promise微任务执行顺序问题 的全部内容, 来源链接: utcz.com/a/82416.html