以下代码均在 nodejs v10.12.0 平台上测试得到结果(浏览器略有差异)
从一道面试题讲起:
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');
// script start
// async1 start
// async 2
// promise1
// script end
// promise2
// async1 end
// setTimeout
涉及的知识点有
- JavaScript异步机制(事件循环机制)
- 微任务和宏任务
其中较难理解的一点是:在async1中,等待async2执行完后(打印出了async2),竟然没有继续执行async1中的剩余代码,而是跳出async1,执行其他同步代码(console.log(‘promise1’))
查了很多博客的解释,有句解释可以帮我很好理解:async函数中的await会跳出当前函数堆栈,交出代码执行权
await 在等待一个Promise,当它等到Promise后,就将执行权交给其他堆栈,然后继续等待Promise的状态变为fullfill或rejected,当 Promise 的状态从pending 改变后,会将对应的回调函数(resolve或reject)加入 microTask 队列中
另一个较难理解的是打印完 script end 后 先打印 promise2 再 async1 end;要理解这个问题还是简化下代码
同步代码执行完后总会去检查任务队列中是否有新的 microTask,有则立即执行所有 microTask
async function async1(){
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2(){
console.log("async2");
}
async1();
Promise.resolve().then(()=>{
console.log("promise");
})
// 正确顺序:
// async1 start
// async2
// promise
// async1 end
首先执行 async1 函数,打印出 “async1 start”;再开始执行 await async2() | logs | async1 start | | :—-: | —- | | tasks | script |
再执行 async2 函数,打印出 async2 ; async2 返回一个 Promise(此时 Promise 的状态是 pending),并跳出当前async 函数(async1),执行其他同步代码或 microTask
| logs | async1 start / async2
| | :—-: | —- | | tasks | script |
这里贴张firefox控制台的运行结果,显示await 到 pending 的 Promise 后就交出执行权了
执行 Promise.resolve() ,因为 Promise.resolve() 返回的是 fullfilled 的 Promise,所以这里的 Promise.then 回调函数被添加入任务队列中 | logs | async1 start / async2 | | :—-: | —- | | tasks | Promise.then |
回到 async1 中的 await async2 返回的结果 Promise(pending),执行它并等待这个 Promise 的状态变为 fullfilled 后,将该 Promise 的then回调加入任务队列(此时 async2 返回的 Promise 的then回调是空函数,但仍然存在) | logs | async1 start / async2 | | :—-: | —- | | tasks | Promise.then / async2:Promise.then
|现在任务队列中存在两个 microTask,首先执行 Promise.then ,打印 promise | logs | async1 start / async2 / promise
| | :—-: | —- | | tasks | async2:Promise.then
|再执行 async2:Promise.then,直到这里 await 的等待大业才算结束,执行 async1 余下任务 | logs | async1 start / async2 / promise / async1 end
| | :—-: | —- | | tasks | (null) |
可是并非是 await 就会交出代码执行权,如果await后面的函数返回的不是 Promise 对象,则不需交出执行权
将 async2 函数改为普通函数:
function async2(){
console.log("async2");
}
// async1 start
// async2
// async1 end
// promise
这时代码等同于同步代码
所以既然 await 等的是 Promise,那返回 Promise 的普通函数能交出执行权吗?将async2 改为返回值为 Promise 对象:
function async2(){
console.log("async2");
return Promise.resolve();
}
// 结果:
// async1 start
// async2
// promise
// async1 end
参考资料: 理解 JavaScript 的 async/await 一道async/await、promise和setTimeout的面试题 今日头条async/await面试题执行顺序 一次弄懂Event Loop(彻底解决此类面试问题)