从eventloop的一道面试题到对async/await的问题

642 阅读3分钟

最开始在群里看到一道面试题

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')
})

console.log('script end')

从各个浏览器运行结果不一样开始

在群里争论了一下,关于在Firefox和Chrome的上运行结果不同的问题。

Chrome

火狐

疑惑在于async1 endpromise2的顺序?

因为不同版本v8引擎对对Promise和Async/Await的处理不同 ,那么这个不同是什么

事实上这个顺序的差异不在于我开始所想的关于eventloop的问题。而是

await 后面是promise 的话,到底发生了什么?

尝试解答

如果用'自动执行器'和Generator 函数模拟一个async。 模拟可能是在火狐在这里一个执行情况。

首先async返回的是一个promise 那么这里模拟的async

const asyncFn = function (fn) {
     return Promise.resolve(spawn(fn))
}

是吧?那么我还需要一个自动执行器

function spawn (gen){
return new Promise((resolve,reject)=>{
  let g = gen()
function step(nextF){
  try{
   next = nextF()

  }catch(e){
    reject(e)
  }
     if (next.done) {
      return resolve(next.value);
}  
  new Promise(resolve=>{
     resolve(next.value)   // Firefox
  }).then(val=>{ 
         step(()=>{
           return g.next(val)
         })
 },
   e=>{
        step(()=>{
          return  g.next(e)
         })
 })   
}
 step(()=>{
  return g.next(undefined)
})
})

}

现在把题目里的async全部替换一下,把setTimeout删掉,代码就等同于下面的。

const async1 = function* () {
   console.log('async1 start')
   yield asyncFn(async2)

   console.log('async1 end')  
}

const asyncFn = function (fn) {
      return Promise.resolve(spawn(fn))
}

function * async2() {
   console.log('async2')         
}

asyncFn(async1)                  
new Promise(function(resolve) {
  console.log('promise1')
  resolve()
}).then(function() {
  console.log('promise2')
}).then(function() {
  console.log('promise3')
})

所以这些问题原因就出在spawn里面的

 new Promise(resolve=>{
      resolve(next.value)   // firefox
   })

这里的next.value是一个promise,这样子就相当于用yield asyncFn(async2)上面的代码实现await asyncFn(async2)。试图更好的解释await promise 这里到底发生了什么?

这就相当于是在说resolve(next.value)发生了什么?

先看下面一段代码

let p1 = function (){
  return new Promise(resolve=>resolve())
}

new Promise(resolve=>resolve(p1())).then(()=>{  //当p1为promise时
  console.log('first')
})
new Promise(resolve=>resolve()).then(()=>{
  console.log('second')
})             
//"second    
//  "first"   延迟了

这种的'延时'在于 resolve(thenable)和resolve('non-thenable-object')的差异。

根据文章作者的内容,spawn里面的代码可以改写为

new Promise(resolve=>{
      Promise.resolve(next.value).then(()=>{
        Promise.resolve().then(()=>resolve())
      })  
   })

这样子,job队列不会先立即执行resolve()

如果把上面代码加两个console.log

new Promise(resolve=>{
      Promise.resolve(next.value).then(()=>{
        console.log('第一个')
        Promise.resolve().then(()=>{
           console.log('第二个')
          resolve()})
      })  
   })

  • 执行new Promise(resolve=>{//.....}),第一层的promise.then进入微任务Event Queue
  • 执行pormise1,后面的第一个promise.then进入微任务Event Queue
  • 第一轮事件循环宏任务执行完,准备执行微任务队列里的任务
  • 执行微任务Event Queue里的第一个microtask,打印出 '第一个',下一层promise.then进入队列。
  • 执行然后打印出'promise2',promise3进入队列
  • 执行然后打印'第二个',后面的promise.then加入队列
  • 执行然后打印'promise3'
  • 执行step(()=>{return g.next(val)}),打印'async1 end',async1执行完毕,done:true

这里面延迟两个microtask,这也是题目在火狐和Chrome73以前版本输出结果async1 end在后面的原因。

同样的

//chrome 73
Promise.resolve(next.value)

这种结果和在chrome 73里面是一致的,Promise.resolve 并不会产生一个新 promise,而是直接返回next.value,这当然就立即执行了。

最后对比一下

//chrome 73
Promise.resolve(next.value)//立即执行
//Firefox和Chrome 70
new Promise(resolve=>{resolve(next.value)}) //延迟两个

平常认为是同样的东西,在这里的结果却完全不一样。

一些结论

  • 其实就是规范的问题
  • 在这里面的差异可能在于Promise.resolve()new Promise(resolve=>resolve())。实际上根据规范他们并不能完全相等。

最后

` 如果觉得有不对的地方,希望可以留言指正交流,同时你觉得有说的不明白或者不完善的地方,可以看参考文章

参考文章

参考规范