8张图帮你一步步看清 async/await 和 promise 的执行顺序

46次阅读

共计 5176 个字符,预计需要花费 13 分钟才能阅读完成。

11 张图让你一步步看清 async/await 和 promise 的执行顺序

为什么写这篇文章?
测试一下自己有没有必要看
需要具备的前置基础知识

主要内容

对于 async await 的理解
画图一步步看清宏任务、微任务的执行过程

为什么写这篇文章?
说实话,关于 js 的异步执行顺序,宏任务、微任务这些,或者 async/await 这些慨念已经有非常多的文章写了。
但是怎么说呢,简单来说,业务中很少用 async,不太懂 async 呢,
研究了一天,感觉懂了,所手痒想写一篇,哈哈
毕竟自己学会的知识,如果连表达清楚都做不到,怎么能指望自己用好它呢?
测试一下自己有没有必要看
所以我写这个的文章,主要还是交流学习,如果您已经清楚了 eventloop/async/await/promise 这些东西呢,可以 break 啦
有说的不对的地方,欢迎留言讨论,
那么还是先通过一道题自我检测一下,是否有必要继续看下去把。
其实呢,这是去年一道烂大街的「今日头条」的面试题。
我觉得这道题的关键,不仅是说出正确的打印顺序,更重要的能否说清楚每一个步骤,为什么这样执行。

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’)

注:因为是一道前端面试题,所以答案是以浏览器的 eventloop 机制为准的,在 node 平台上运行会有差异。
script start
async1 start
async2
promise1
script end
promise2
async1 end
setTimeout
如果你发现运行结果跟自己想的一样,可以选择跳过这篇文章啦,
或者如果你有兴趣看看俺俩的理解有没有区别,可以跳到后面的「画图讲解的部分」
需要具备的前置知识

promise 的使用经验
浏览器端的 eventloop

不过如果是对 ES7 的 async 不太熟悉,是没关系的哈,因为这篇文章会详解 async。
那么如果不具备这些知识呢,推荐几篇我觉得讲得比较清楚的文章

https://segmentfault.com/a/11… 这是我之前写的讲解 eventloop 的文章,我觉得还算清晰,但是没涉及 async

https://segmentfault.com/a/11… 这是我读过的讲 async await 最清楚的文章

http://es6.ruanyifeng.com/#do… promise 就推荐阮一峰老师的 ES6 吧,不过不熟悉 promise 的应该较少啦。

主要内容
第 1 部分:对于 async await 的理解
我推荐的那篇文章,对 async/await 讲得更详细。不过我希望自己能更加精炼的帮你理解它们
这部分,主要会讲解 3 点内容

1.async 做一件什么事情?
2.await 在等什么?
3.await 等到之后,做了一件什么事情?
4. 补充: async/await 比 promise 有哪些优势?(回头补充)

1.async 做一件什么事情?
一句话概括:带 async 关键字的函数,它使得你的函数的返回值必定是 promise 对象
也就是
如果 async 关键字函数返回的不是 promise,会自动用 Promise.resolve() 包装
如果 async 关键字函数显式地返回 promise,那就以你返回的 promise 为准
这是一个简单的例子,可以看到 async 关键字函数和普通函数的返回值的区别
async function fn1(){
return 123
}

function fn2(){
return 123
}

console.log(fn1())
console.log(fn2())
Promise {<resolved>: 123}

123
所以你看,async 函数也没啥了不起的,以后看到带有 async 关键字的函数也不用慌张,你就想它无非就是把 return 值包装了一下,其他就跟普通函数一样。
关于 async 关键字还有那些要注意的?

在语义上要理解,async 表示函数内部有异步操作
另外注意,一般 await 关键字要在 async 关键字函数的内部,await 写在外面会报错。

2.await 在等什么?
一句话概括:await 等的是右侧「表达式」的结果
也就是说,
右侧如果是函数,那么函数的 return 值就是「表达式的结果」
右侧如果是一个 ‘hello’ 或者什么值,那表达式的结果就是 ‘hello’
async function async1() {
console.log(‘async1 start’)
await async2()
console.log(‘async1 end’)
}
async function async2() {
console.log(‘async2’)
}
async1()
console.log(‘script start’)
这里注意一点,可能大家都知道 await 会让出线程,阻塞后面的代码,那么上面例子中,‘async2’ 和 ‘script start’ 谁先打印呢?
是从左向右执行,一旦碰到 await 直接跳出, 阻塞 async2() 的执行?
还是从右向左,先执行 async2 后,发现有 await 关键字,于是让出线程,阻塞代码呢?
实践的结论是,从右向左的。先打印 async2,后打印的 script start
之所以提一嘴,是因为我经常看到这样的说法,「一旦遇到 await 就立刻让出线程,阻塞后面的代码」
这样的说法,会让我误以为,await 后面那个函数,async2() 也直接被阻塞呢。
3.await 等到之后,做了一件什么事情?
那么右侧表达式的结果,就是 await 要等的东西。
等到之后,对于 await 来说,分 2 个情况

不是 promise 对象
是 promise 对象

如果不是 promise , await 会阻塞后面的代码,先执行 async 外面的同步代码,同步代码执行完,再回到 async 内部,把这个非 promise 的东西,作为 await 表达式的结果
如果它等到的是一个 promise 对象,await 也会暂停 async 后面的代码,先执行 async 外面的同步代码,等着 Promise 对象 fulfilled,然后把 resolve 的参数作为 await 表达式的运算结果。
第 2 部分:画图一步步看清宏任务、微任务的执行过程
我们以开篇的经典面试题为例,分析这个例子中的宏任务和微任务。
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’)
先分享一个我个人理解的宏任务和微任务的慨念,在我脑海中宏任务和为微任务如图所示

也就是「宏任务」、「微任务」都是队列。
一段代码执行时,会先执行宏任务中的同步代码,

如果执行中遇到 setTimeout 之类宏任务,那么就把这个 setTimeout 内部的函数推入「宏任务的队列」中,下一轮宏任务执行时调用。
如果执行中遇到 promise.then() 之类的微任务,就会推入到「当前宏任务的微任务队列」中,在本轮宏任务的同步代码执行都完成后,依次执行所有的微任务 1、2、3

下面就以面试题为例子,分析这段代码的执行顺序.
每次宏任务和微任务发生变化,我都会画一个图来表示他们的变化。
直接打印同步代码 console.log(‘script start’)
首先是 2 个函数声明,虽然有 async 关键字,但不是调用我们就不看。然后首先是打印同步代码 console.log(‘script start’)

将 setTimeout 放入宏任务队列
默认 <script></script> 所包裹的代码,其实可以理解为是第一个宏任务,所以这里是宏任务 2

调用 async1,打印 同步代码 console.log(‘async1 start’)
我们说过看到带有 async 关键字的函数,不用害怕,它的仅仅是把 return 值包装成了 promise,其他并没有什么不同的地方。所以就很普通的打印 console.log(‘async1 start’)

分析一下 await async2()
前文提过 await,1. 它先计算出右侧的结果,2. 然后看到 await 后,中断 async 函数

– 先得到 await 右侧表达式的结果。执行 async2(),打印同步代码 console.log(‘async2’), 并且 return Promise.resolve(undefined)
– await 后,中断 async 函数,先执行 async 外的同步代码

目前就直接打印 console.log(‘async2’)

被阻塞后,要执行 async 之外的代码
执行 new Promise(),Promise 构造函数是直接调用的同步代码,所以 console.log( ‘promise1’)

代码运行到 promise.then()
代码运行到 promise.then(),发现这个是微任务,所以暂时不打印,只是推入当前宏任务的微任务队列中。
注意:这里只是把 promise2 推入微任务队列,并没有执行。微任务会在当前宏任务的同步代码执行完毕,才会依次执行

打印同步代码 console.log(‘script end’)
没什么好说的。执行完这个同步代码后,「async 外的代码」终于走了一遍

下面该回到 await 表达式那里,执行 await Promise.resolve(undefined) 了

回到 async 内部,执行 await Promise.resolve(undefined)
这部分可能不太好理解,我尽量表达我的想法。
对于 await Promise.resolve(undefined) 如何理解呢?
https://developer.mozilla.org…
根据 MDN 原话我们知道
如果一个 Promise 被传递给一个 await 操作符,await 将等待 Promise 正常处理完成并返回其处理结果。
在我们这个例子中,就是 Promise.resolve(undefined) 正常处理完成,并返回其处理结果。那么 await async2() 就算是执行结束了。
目前这个 promise 的状态是 fulfilled,等其处理结果返回就可以执行 await 下面的代码了。
那何时能拿到处理结果呢?
回忆平时我们用 promise,调用 resolve 后,何时能拿到处理结果?是不是需要在 then 的第一个参数里,才能拿到结果。
(调用 resolve 时,会把 then 的参数推入微任务队列,等主线程空闲时,再调用它)
所以这里的 await Promise.resolve() 就类似于
Promise.resolve(undefined).then((undefined) => {

})
把 then 的第一个回调参数 (undefined) => {} 推入微任务队列。
then 执行完,才是 await async2() 执行结束。
await async2() 执行结束,才能继续执行后面的代码
如图

此时当前宏任务 1 都执行完了,要处理微任务队列里的代码。
微任务队列,先进选出的原则,

执行微任务 1,打印 promise2
执行微任务 2,没什么内容..

但是微任务 2 执行后,await async2() 语句结束,后面的代码不再被阻塞,所以打印
console.log(‘async1 end’)
宏任务 1 执行完成后, 执行宏任务 2
宏任务 2 的执行比较简单,就是打印
console.log(‘setTimeout’)
补充
写这篇文章之前,我也没想到描述这个过程如此麻烦。最后还是觉得自己说得并不是很清楚 …
这段代码总体也是比较绕的
如果不理解可以留言,有错误的话也欢迎指正。

正文完
 0