前段时间总结了几篇关于异步原理、Promise原理、Promise面试题、async/await 原理的文章,链接如下,感兴趣的可以去看下,相信会有所收获。
10道 Promise 面试题彻底理解 Promise 异步执行顺序
async await 原理解析之爱上 async/await
本篇文章准备一个代码实例来阐述async/await、promise、setTimeout(宏任务、微任务)之间的执行顺序,做一个最终总结。理论终究是理论,枯燥难懂,对于程序猿来说,最好的还是代码实例。所以就找了一个非常有代表性的面试题。
目标:不是写出正确的执行顺序,而是说清楚每一个步骤,为什么这么执行。
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('proimse1')
resolve()
}).then(function() {
console.log('promise2')
})
console.log('script end')
这个代码实例算是很经典的一道题了,其中涉及到了 js 的 eventloop、promise、async、await 以及定时器。
1. 注意点
很显然,这考察的是 JavaScript 中的 事件循环
和 回调队列
,需要注意以下几点:
- Promise 的优先级高于
setTimeout
微任务,所以,setTimeout
回调会在最后执行。 - Promise 一旦被定义,就会立即执行。
Promise
的resolve
与reject
是异步执行的回调。所以,resolve
会被放到回调队列中,在主函数执行完 和宏任务 setTimeout
执行前调用。- await 会阻塞后面的任务,指的是下一行代码,await 的同行代码是会立即执行的。
await
执行完后,会让出线程。async
标记的函数会返回一个Promise 对象
2. 代码分析
首先分析下代码,发现里面有 同步代码、微任务、宏任务。
一段代码执行时,会先执行宏任务中的同步代码,
- 如果执行中遇到
setTimeout
之类的宏任务,就会把这个setTimeout
内部的函数放到【宏任务的队列】中,下一轮宏任务执行时调用。 - 如果执行中遇到
Promise.then()
之列的异步微任务,就会把异步微任务放到【当前宏任务的微任务队列】中,在本轮宏任务的同步代码都执行完成后,依次执行所有的异步微任务:task1、task2、task3…
在每一层(一次)的事件循环中,首先整体代码块看做一个宏任务,宏任务中的 Promise(then、catch、finally )、MutationObserver、Process.nextTick
就是该宏任务层的微任务。宏任务中的同步代码进入主线程中是立即执行的,宏任务中的非微任务的异步代码(比如定时器)将作为下一次循环时的宏任务进入的调用栈等待执行。此时,调用栈中的等待执行队列分为两种,分别是优先级较高的本层循环中的微任务队列,以及优先级低的下次循环执行的宏任务队列。
每一个宏任务队列都可以理解为当前的主线程,JavaScript 总是先执行主线程上的任务,执行完 毕后执行当前宏任务队列上的所有微任务,先进先出原则,在执行完当前宏任务队列上的所有微任务之后,才会执行下一个宏任务。
3. 执行顺序解析:
执行顺序解析:
1、js是单线程的,首先执行主线程上的任务 console.log('script start')
输出:script start
2、遇到setTimeout()定时器,这是一个宏任务,放入到下一个宏任务队列中,等待当前宏任务以及其微任务队列执行完毕再执行。
3、执行 async1() 函数,实质上是创建了一个Promise对象,而promise的构造函数的运行是在主任务队列中的,所以会立即执行 console.log('async1 start')
输出: async1 start
4、执行 await async2()。我们知道,await 会立即执行同行代码,阻塞下一行代码,
(await 也会暂停async后面的代码,先执行async外面的同步代码)
流程进入 async2()函数,并返回 Promise 对象,即返回 async2.then(() => {console.log('async1 end')})。
这里就会把 .then() 里面的内容放到当前宏任务的微任务队列中(即await 阻塞下一行代码),将其命名为task1.
此时task1并没有执行,因为微任务会在当前宏任务的同步代码执行完成后,才会依次执行。
同时也会执行 async2() 的构造函数,输出async2
输出:async2
ps: 这个地方可能有人会看不懂,请看下面解析。
5、执行 new Promise(),当我们 new 一个 Promise 时,传入的回调函数(构造函数)为同步代码,会立即执行。
输出:promise1
6、执行 resolve() 函数,那么会进入到 then() 中,
我们知道,Promise.then() 是一个异步微任务,所以会被放到当前宏任务的微任务队列中,,将其命名为task2。
此时task2并没有执行,因为微任务会在当前宏任务的同步代码执行完成后,才会依次执行。
7、执行最后的主线程任务:console.log('script end')
输出:script end
8、此时宏任务1中的同步代码已经执行完成,开始依次处理微任务队列中的代码
遵循:先进先出原则。
输出:async1 end ; promise2
9、在最后执行下一个宏任务队列,即setTimeout
输出:setTimeout
所以最终输出结果为:
4. 额外解析
对于上面的流程解析,可能有人对第4步不太理解,首先我们明确一个概念:async/await 实质上是 Promise.then
的语法糖,带 async
关键字的函数,会让函数返回一个 Promise
对象。
其实,async1() 函数
可以写成以下方式,便于理解:
async function async1() {
console.log('async1 start')
async2().then(_ => {
console.log('async1 end')
})
}
如果 return
的不是 promise,会自动用 Promise.resolve()
包装,就以代码实例中的 async2() 为例,返回的就是 return Promise.resolve(undefined)
如果 async 关键字函数显式的返回 Promise
,则以此为准。
对于 await
来说,如果 await 后面不是 Promise 对象
,那么 await 会阻塞后面的代码,先执行 async 函数外面的同步代码,同步代码执行完毕后,再回到 async 内部,把这个 非 Promise 的东西,作为 await 表达式的结果,然后在执行下面的代码。
如果 await 后面是 Promise 对象,await 也会阻塞后面的代码,在 async 外部的同步代码执行完成之后等到 promise 对象 fulfilled,然后把 resolve 的参数作为await 表达式的运行结果
5. 分析下 await
我们知道,await 会让出线程,阻塞后面的代码。那么在代码执行中,是一旦碰到 await 直接跳出,阻塞 async2() 的执行?还是先执行 async2() ,发现有 await 关键字
,于是让出线程,阻塞代码呢?
从上面的实践可以得出结论:代码是从右向左执行,先执行,发现有await关键字后,让出线程,阻塞代码。
结论:
- 它先计算出右侧的结果;
- 看到 await 后,中断 async 函数,先执行 async 外的同步代码。
7. 结尾
我的异步系列更新到这里基本上就算完结了,不过学习永不结束!
我把上面的代码实例稍微改动了下,大家可以看下这个会输出什么结果,如果上述解析看懂了的话,下面的代码也难不倒大家,答案可以写在评论区,欢迎大家讨论、交流。
console.log('start')
new Promise(function(resolve) {
console.log('promise1')
setTimeout(() => {
console.log('timer1')
},0)
resolve()
}).then(() => {
console.log('promise2')
setTimeout(() => {
console.log('timer2')
},0)
})
const promise1 = Promise.resolve().then(() => {
console.log('promise3')
setTimeout(() => {
console.log('timer3')
},0)
})
async function async1() {
console.log('async1')
await async2()
console.log('async1 end');
}
async function async2 () {
console.log('async2')
}
async1()
setTimeout(() => {
console.log('timer4')
},0)
console.log('script end')