前段时间总结了几篇关于异步原理、Promise原理、Promise面试题、async/await 原理的文章,链接如下,感兴趣的可以去看下,相信会有所收获。
一篇文章理清JavaScript中的异步操作原理
Promise原理及执行顺序详解
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 以及定时器。
很显然,这考察的是 JavaScript 中的 事件循环
和 回调队列
,需要注意以下几点:
setTimeout
微任务,所以, setTimeout
回调会在最后执行。Promise
的 resolve
与 reject
是异步执行的回调。所以,resolve
会被放到回调队列中,在主函数执行完 和 宏任务 setTimeout
执行前调用。await
执行完后,会让出线程。async
标记的函数会返回一个 Promise 对象
首先分析下代码,发现里面有 同步代码、微任务、宏任务。
一段代码执行时,会先执行宏任务中的同步代码,
setTimeout
之类的宏任务,就会把这个 setTimeout
内部的函数放到【宏任务的队列】中,下一轮宏任务执行时调用。Promise.then()
之列的异步微任务,就会把异步微任务放到【当前宏任务的微任务队列】中,在本轮宏任务的同步代码都执行完成后,依次执行所有的异步微任务:task1、task2、task3…
在每一层(一次)的事件循环中,首先整体代码块看做一个宏任务,宏任务中的 Promise(then、catch、finally )、MutationObserver、Process.nextTick
就是该宏任务层的微任务。宏任务中的同步代码进入主线程中是立即执行的,宏任务中的非微任务的异步代码(比如定时器)将作为下一次循环时的宏任务进入的调用栈等待执行。此时,调用栈中的等待执行队列分为两种,分别是优先级较高的本层循环中的微任务队列,以及优先级低的下次循环执行的宏任务队列。
每一个宏任务队列都可以理解为当前的主线程,JavaScript 总是先执行主线程上的任务,执行完 毕后执行当前宏任务队列上的所有微任务,先进先出原则,在执行完当前宏任务队列上的所有微任务之后,才会执行下一个宏任务。
执行顺序解析:
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步不太理解,首先我们明确一个概念: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 表达式的运行结果
我们知道,await 会让出线程,阻塞后面的代码。那么在代码执行中,是一旦碰到 await 直接跳出,阻塞 async2() 的执行?还是先执行 async2() ,发现有 await 关键字
,于是让出线程,阻塞代码呢?
从上面的实践可以得出结论:代码是从右向左执行,先执行,发现有await关键字后,让出线程,阻塞代码。
结论:
我的异步系列更新到这里基本上就算完结了,不过学习永不结束!
我把上面的代码实例稍微改动了下,大家可以看下这个会输出什么结果,如果上述解析看懂了的话,下面的代码也难不倒大家,答案可以写在评论区,欢迎大家讨论、交流。
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')