JS系列 | 当 Event Loop 遇上 async await

写在前面的话

Event Loop 包括其相关概念,也许大家都懂了。但是 Event Loop 碰上 async await,执行顺序也许就和期望不一致。这到底是为什么呢?我花了几天时间去查资料和思考,但是还是没有捋顺,只是猜测 async 函数至少比普通函数多一步 then操作,但是不知道怎么证实。网上很多资料都说了 await 相当于 Promise.resolve 的语义,await 下面的代码会放入 Promise.resolve(arg).then 回调中。但是,当 async 函数中没有 await 的时候,async 函数的执行会和普通函数一样嘛?我查到的资料都没有解决我的疑问。后来,我男朋友帮我把 async 函数用 babel 编译了一遍,拔了一下源码,在源码中证实了我的猜测。async 函数中肯定有一次停止上下文的微任务。

举个🌰

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

babel 转译后

babel 转译

再看一下 async 函数每一次执行的源码【向下兼容使用 generator 】

asyncGeneratorStep

我的理解是 async 函数里的下一个 await 到前一个 await之间的代码,以及第一个 await 之前的代码,会对应 switch case 中的一个 case 分支。若没有 await ,第一个 case 0 分支就是整段代码,下一个 case "end" 就是停止上下文。上一个 case 所 return 的 value 作为 Promise.resolve() 的参数,下一个 case 的操作作为 Promise.resolve(value).then 中的回调,并放入微任务队列。如果 Promise.resolve 接收的参数是一个 promise ,则 Promise.resolve 后续的 then、catch 操作都依赖于参数 promise 的状态。

来人,上代码

// 在 chrome 浏览器中执行
console.log('script start')

async function async1() {
    console.log('async1 start')
    await async2()
    console.log('async1 end')
}
async function async2() {
    console.log('async2 start')
    return Promise.resolve().then(()=>{
      console.log('async2 end')
    })
}
/* 这个函数没加 async 关键字,运行结果会不一致
function async2() {
    console.log('async2 start')
    return Promise.resolve().then(()=>{
      console.log('async2 end')
    })
}
*/
async1()

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

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

console.log('script end')

script start
async1 start
async2 start
new promise
script end
async2 end
promise1
promise2
async1 end
setTimeout

解析
1.宏任务 - script 执行: log 'script start'
2.执行 async1 函数: log 'async1 start'
3.执行 async2 函数: log 'aync2 start' 【microTask: log 'async2 end'】 注意!此时 async2 函数还未全部执行完,还有一步停止上下文的操作,所以此时 async1 函数中 await 下面的代码还没到推入微队列的时机
4.执行 setTimeout 函数: 回调函数注册到 macroTask 队列中 【macroTask: log 'setTimeout'】
5.new Promise 构造函数立即执行: log 'new promise'
6.走到 new Promise 第一个 then 方法: 注册一个微任务 【microTask: log 'async2 end',log 'promise1'】
7.宏任务 - script 执行完毕: log 'script end'
8.script 执行完毕后,从微任务队列取微任务执行,并将 'stop async2 contenxt' 的微任务推入队列: log 'async2 end' 【microTask:log 'promise1' ,'stop async2 contenxt'】
9.从 microTask 取下一个消息:log 'promise1'。走到 then 操作,注册一个微任务 【microTask:'stop sync2 context', log 'promise2' 】
10.从 microTask 取下一个消息:'stop async2 context',此时 async2 执行完毕,可以将 async1函数中 await 下面的代码push进微任务队列。【microTask:log 'promise2',log 'async1 end'】
11.从 microTask 取下一个消息:log 'promise2' 【microTask:log 'async1 end'】
12.从 microTask 取下一个消息:log 'async1 end'
13.执行下一个宏任务: log 'setTimeout'


深度解析执行过程

提前声明

  • 每个回调取了一个代号,方便阅读,请记在心中
let promiseCallBack1 = () => {
    console.log('async2 end')
}

let setTimeoutCallBack = function() {
    console.log('setTimeout')
}

let promiseCallBack2 = function() {
    console.log('promise1')
}

let promiseCallBack3 = function() {
    console.log('promise2')
}

let promiseCallBack4 = function() {
    console.log('async1 end')
}

let stopContext = function() {
    _context.stop()
}
  • await等的是右侧表达式的结果,从右往左执行

重点来了

1. 执行 async1 函数,间接执行 async2 函数

  • 左边的执行栈:先 push async1 EC,再 push async2 EC
  • 中间的Event Queue:将 promiseCallBack1 注册到 microTask Queue 【注意只是注册,并未执行】
  • 执行完毕后,async2 EC 和 async1 EC 相继出栈
let promiseCallBack1 = () => {
    console.log('async2 end')
}

执行 async1

补充说明

async2 函数相当于以下操作:

  • promise1 [[pending]] = Promise.resolve().then(() => { console.log('async2 end') } ) 【then 回调还没执行,所以promise1 还不能是 resolved 状态】
  • promise2 [[pending]] = Promise.resolve(promise1).then(() = > { _context.stop() })

async1 函数相当于以下操作:

  • promise3 [[pending]] = Promise.resolve(promise2).then(() => { console.log('async1 end') })
  • promise4 [[pending]] = Promise.resolve(promise3).then(() = > { _context.stop() })
    babel-async2
    babel-async1

2. 执行 setTimeout 函数时

  • 左边的执行栈:压入 setTimeout EC
  • 中间的Event Queue:将 setTimeoutCallBack 注册到 macroTask Queue
  • 执行完毕后,setTimeout EC出栈
let setTimeoutCallBack = function() {
    console.log('setTimeout')
}

执行 setTimeout


3. 执行 new Promise 函数时

  • 左边的执行栈:压入 Promise EC
  • 中间的Event Queue:将 promiseCallBack2 注册到 macroTask Queue
  • 执行完毕后,弹出 Promise EC,此时执行栈为空
let promiseCallBack2 = function() {
    console.log('promise1')
}

执行 new Promise


4. 执行栈为空,从 microTask Queue 取消息 promiseCallBack1

  • 左边的执行栈:push promiseCallBack1 EC,然后执行 promiseCallBack1 (打印 async2 end)
  • 中间的Event Queue:将 stopContext 注册到 macroTask Queue
  • 执行完毕后,弹出 promiseCallBack1 EC,此时执行栈为空

promiseCallBack1

补充说明

async2 函数相当于以下操作:

  • promise1 [[resolved]] = Promise.resolve().then(() => { console.log('async2 end') } 【 then回调执行完毕,状态变为 resolved 】
  • promise2 [[pending]] = Promise.resolve(promise1).then(() = > { _context.stop() } 【 promise1 resolved 表示可以走到 then,将回调注册为微任务】

async1 函数相当于以下操作:

  • promise3 [[pending]] = Promise.resolve(promise2).then(() => { console.log('async1 end') })
  • promise4 [[pending]] = Promise.resolve(promise3).then(() = > { _context.stop() }

5. 执行栈为空,从 microTask Queue 取消息 promiseCallBack2

  • 左边的执行栈:push promiseCallBack2 EC,然后执行 promiseCallBack2 (打印 promise1)
  • 中间的Event Queue:执行完毕后将 promiseCallBack3 回调注册到 microTask Queue
  • 执行完毕后,弹出 promiseCallBack2 EC,此时执行栈为空
let promiseCallBack3 = function() {
    console.log('promise2')
}

promiseCallBack2


6.执行栈为空,从 microTask Queue 取消息 stopContext

  • 左边的执行栈:先压入 async2 EC 再压入 async1 EC
  • 中间的Event Queue:执行完毕后将 promiseCallBack4 回调注册到 microTask Queue
  • 执行完毕后,弹出 async2 EC 和 async1 EC,此时执行栈为空
let promiseCallBack4 = function() {
    console.log('async1 end')
}

stopContext

补充说明

async2 函数相当于以下操作:

  • promise1 [[resolved]] = Promise.resolve().then(() => { console.log('async2 end') }
  • promise2 [[resolved]] = Promise.resolve(promise1).then(() = > { _context.stop() }

async1 函数相当于以下操作:

  • promise3 [[pending]] = Promise.resolve(promise2).then(() => { console.log('async1 end') }) 【 promise2 resolved 表示可以走到 then,把回调注册为微任务】
  • promise4 [[pending]] = Promise.resolve(promise3).then(() = > { _context.stop() }

7. 执行栈为空,从 microTask Queue 取消息 promiseCallBack3

  • 左边的执行栈:压入 promiseCallBack3 EC,然后执行 promiseCallBack3 (打印 promise2)
  • 中间的Event Queue:没有注册任何回调
  • 执行完毕后,弹出 promiseCallBack3 EC,此时执行栈为空

promiseCallBack3


8. 执行栈为空,从 microTask Queue 取消息 promiseCallBack4 执行 (打印 async1 end)

  • 左边的执行栈:压入 async1 EC
  • 中间的Event Queue:将 stopContext 注册到 macroTask Queue
  • 执行完毕后,弹出 async1 EC,此时执行栈为空

promiseCallBack4

补充说明

async2 函数相当于以下操作:

  • promise1 [[resolved]] = Promise.resolve().then(() => { console.log('async2 end') }
  • promise2 [[resolved]] = Promise.resolve(promise1).then(() = > { _context.stop() }

async1 函数相当于以下操作:

  • promise3 [[resolved]] = Promise.resolve(promise2).then(() => { console.log('async1 end') })
  • promise4 [[pending]] = Promise.resolve(promise3).then(() = > { _context.stop() } 【 promise3 resolved 表示可以走到 then,把回调注册为微任务 】

9. 执行栈为空,从 microTask Queue 取消息 stopContext 执行

  • 左边的执行栈:压入 async1 EC
  • 中间的Event Queue:没有注册任何回调
  • 执行完毕后,弹出 async1 EC,此时执行栈为空,macroTask Queue 也为空

补充说明

async2 函数相当于以下操作:

  • promise1 [[resolved]] = Promise.resolve().then(() => { console.log('async2 end') }
  • promise2 [[resolved]] = Promise.resolve(promise1).then(() = > { _context.stop() }

async1 函数相当于以下操作:

  • promise3 [[resolved]] = Promise.resolve(promise2).then(() => { console.log('async1 end') })
  • promise4 [[resolved]] = Promise.resolve(promise3).then(() = > { _context.stop() } 【 stopContext 执行完毕后,promise4由 pending 变为 resolved 】

微任务队列已清空,开启第二轮事件循环,从宏任务队列取下一个宏任务

10. 执行栈为空,从 macroTask Queue 取消息 setTimeoutCallBack

  • 左边的执行栈:压入 setTimeoutCallBack EC,执行 setTimeoutCallBack (打印 setTimeout)
  • 中间的Event Queue:没有注册任何回调
  • 执行完毕后,弹出 setTimeoutCallBack EC,此时执行栈为空

setTimeoutCallBack

对比执行

// 在 chrome 浏览器中执行
console.log('script start')

async function async1() {
    console.log('async1 start')
    await async2()
    console.log('async1 end')
}
// 这个函数没加 async 关键字,运行结果会不一致
function async2() {
    console.log('async2 start')
    return Promise.resolve().then(()=>{
      console.log('async2 end')
    })
}
async1()

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

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

console.log('script end')

// 没加 async 关键字 的结果
script start
async1 start
async2 start
new promise
script end
async2 end
promise1
async1 end // 提前一个时序打印 
promise2
setTimeout

// 加了async 关键字 的结果
script start
async1 start
async2 start
new promise
script end
async2 end
promise1
promise2
async1 end
setTimeout

普通函数不像 async 函数有一步停止上下文的操作,所以会提前一个时序

写在最后的话

查阅很多资料以后,你会发现有些资料之间会有矛盾。或者说,你也查不到自己想要的资料。或者说,ECMA 的新规范晦涩难懂。这个时候,可以使用 babel 将 async 向下兼容,试着理解一下转换后的代码,这样也许会对 Event Loop 有新的理解。

课外拓展

  1. await 将直接使用 Promise.resolve() 相同语义

参考资料如下

async/await
Promise.resolve 解析

  1. thenable 对象是带有 then 方法的对象或者函数: thenable
  2. 将 thenable 对象转为 promise 对象,需要在微任务队列中先加入一个 PromiseResolveThenableJob: PromiseResolveThenableJob