写在前面的话
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 转译后

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

我的理解是 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')
}

补充说明
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() })


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

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

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

补充说明
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')
}

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

补充说明
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,此时执行栈为空

8. 执行栈为空,从 microTask Queue 取消息 promiseCallBack4 执行 (打印 async1 end)
- 左边的执行栈:压入 async1 EC
- 中间的Event Queue:将 stopContext 注册到 macroTask Queue
- 执行完毕后,弹出 async1 EC,此时执行栈为空

补充说明
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,此时执行栈为空

对比执行
// 在 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 有新的理解。
课外拓展
- await 将直接使用 Promise.resolve() 相同语义
参考资料如下
async/await
Promise.resolve 解析
- thenable 对象是带有 then 方法的对象或者函数: thenable
- 将 thenable 对象转为 promise 对象,需要在微任务队列中先加入一个 PromiseResolveThenableJob: PromiseResolveThenableJob