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 start
async1 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 start
async1 start
async2
promise1
script end
async1 end // 与 chrome 70 不一致
promise2 // 与 chrome 70 不一致
promise3 // 与 chrome 70 不一致
promise4
promise5
promise6
promise7
promise8
setTimeout
阅读 21.7k
9 个回答

基础知识

在你看答案之前,我希望你至少了解

  • 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 end
promise2
promise3
promise4

而在 chrome 70 上返回

promise2
promise3
async1 end
promise4

正文

我对 promise 稍微熟悉些,其实也不熟,但是把 await 转成 promise 会相对好理解些,不知道有没有同感?

这道题其实问的是

await async2() 怎么理解?

因为 async 函数总是返回一个 promise,所以其实就是在问

await promise 怎么理解?

那么我们看下规范 Await

clipboard.png

根据提示

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 上得到的都是一样的。

promise2
promise3
async1 end
promise4

Await 规范的更新

那么为什么,chrome 73 现在得到的结果不一样了呢?修改的决议在这里,目前是这个状态。

clipboard.png

就像你所看到的一样,为什么要把 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

clipboard.png

所以 Promise.resolve(promise) 返回 promise, 即Promise.resolve(async2()) 等价于 async2() ,所以最终得到了代码

async function async1 () {
  async2().then(() => {
    console.log('async1 end')
  })
}

这就是贺老师在知乎里所说的

根据 TC39 最近决议,await将直接使用Promise.resolve()相同语义。

tc39 的 spec 的更改体现在

clipboard.png

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点:

  1. async 函数的返回值
  2. Promise 链式 then() 的执行时机
  3. async 函数中的 await 操作符到底做了什么

下面一一回答:

  1. async 函数的返回值:

    • 被 async 操作符修饰的函数必然返回一个 Promise 对象
    • 当 async 函数返回一个值时,Promise 的 resolve 方法负责传递这个值
    • 当 async 函数抛出异常时,Promise 的 reject 方法会传递这个异常值
    • 所以,以示例代码中 async2 为例,其等价于

      function async2(){
        console.log('async2');
        return Promise.resolve();
      }
  2. 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')才会被推入微任务队列,这是一个关键点
  3. 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之后的函数执行完毕之后再执行

chrome 87 运行结果又变了,难受
image.png

你换node环境执行下

标准应该没规定,我当时自己测的chrome和firefox执行顺序不一样的

刚刚测了一下,edge和chrome表现不一样的,反正这个微任务执行顺序标准没规定,具体表现要看浏览器自己怎么处理了


现在有标准了,以后所有浏览器应该有一致的表现(说实话,都是异步,除了js引擎开发者,关注谁先谁后意义没什么意义)

我的理解是,reslove调用后,会把promise2加入到任务队列,然后执行一次Event Loop

reslove调用后,会把promise2加入到任务队列,然后执行一次Event Loop

推荐问题
宣传栏