原文发布在掘金社区: http://www.javashuo.com/article/p-ttehysyl-bu.html
2019年了,相信你们对 Promise 和 async/await 都再也不陌生了。javascript
前几日,我在社区读到了一篇关于 async/await 执行顺序的文章《「前端面试题系列1」今日头条 面试题和思路解析》。文中提到了一道“2017年「今日头条」的前端面试题”,还有另外一篇对此题的解析文章《8张图让你一步步看清 async/await 和 promise 的执行顺序》,两文中都对问题进行了分析。不过在我看来,这两篇文章都没有把这个问题说清楚,同时在评论区中也有不少朋友留言表达了本身的疑惑。前端
其实解决这个问题最关键的是如下两点:java
Promise.resolve(v)
不等于new Promise(resolve => resolve(v))
- 浏览器怎样处理
new Promise(resolve => resolve(thenable))
,即在 Promise 中 resolve 一个 thenable 对象
国际惯例,先给出面试题和答案:node
注:执行顺序以 Chrome71 为准
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')
答案:git
script start async1 start async2 promise1 script end promise2 async1 end setTimeout
看完答案后,我与不少人同样不管如何也不理解 为何 async1 end
会晚于promise2
输出……个人第一反应是 我对 await 的理解有误差,因此我决心要把这个问题弄明白。github
本文主要解释浏览器对 await 的处理,并一步步将原题代码转换为原生Promsie实现。面试
全部执行顺序以 Chrome71 为准,不讨论 Babel 和 Promise 垫片。segmentfault
第一次发文,不免有一些不严谨之处,若有错误,还望你们在评论区批评指正!promise
在解释答案以前,你须要先掌握:浏览器
Promise 基础
宏任务与微任务的优先级
问题主要涉及如下4点:
new Promise(resolve => resolve(thenable))
的处理下面,让咱们一步步将原题中的代码转换为更容易理解的等价代码。
在正式开始以前,咱们先来看如下这段代码:
new Promise((r) => { r(); }) .then(() => console.log(1)) .then(() => console.log(2)) .then(() => console.log(3)) new Promise((r) => { r(); }) .then(() => console.log(4)) .then(() => console.log(5)) .then(() => console.log(6))
答案:
1 4 2 5 3 6
若是你得出的答案是 1 2 3 4 5 6
那说明你尚未很好的理解 Promise.prototype.then()
。
为何要先放出这段代码?
由于 async/await
可视为 Promise 的语法糖,一样基于微任务实现;本题主要纠结的点在于 await 到底作了什么致使 async1 end
晚于 promise2
输出。问题的关键在于其执行过程当中的微任务数量,下文中咱们须要用上述代码中的方式对微任务的执行顺序进行标记,以辅助咱们理解这其中的执行过程。
then()
链式调用,并非连续的建立了多个微任务并推入微任务队列,由于 then()
的返回值必然是一个 Promise,然后续的 then()
是上一步 then()
返回的 Promise 的回调resolve()
,将 Promise 的状态改变为 <resolved>: undefined
, 而后 then 中传入的回调函数 console.log('1')
做为一个微任务被推入微任务队列then()
中传入的回调函数 console.log('2')
此时尚未被推入微任务队列,只有上一个 then()
中的 console.log('1')
执行完毕后,console.log('2')
才会被推入微任务队列Promise.prototype.then()
会隐式返回一个新 Promisethen
会在该 Promise 上注册一个回调,当其状态发生变化时,对应的回调将做为一个微任务被推入微任务队列then()
会当即建立一个微任务,将传入的对应的回调推入微任务队列为了更好的解析问题,下面我对原题代码进行一些修改,剔除和主要问题无关的代码
async function async1() { console.log('async1 start') await async2() console.log('async1 end') } async function async2() { console.log('async2') } async1(); new Promise((resolve) => { console.log(1) resolve() }).then(() => { console.log(2) }).then(() => { console.log(3) }).then(() => { console.log(4) })
答案:
async1 start async2 1 2 3 async1 end 4
咱们剔除了 setTimeout
和一些同步代码,而后为 Promise
的 then
链增长了一个回调,而最终结果中 async1 end 在 3 后输出,而不是在 2 后!
await
必定是作了一些咱们不理解的“诡异操做”,令其后续代码 console.log('async1 end')
被推迟了2个时序。
换句话说,async/await
是 Promise 的语法糖,一样基于微任务实现,不可能有其余超出咱们理解的东西,因此能够判定:在 console.log('async1 end')
执行前,额外执行了2个微任务,因此致使被推迟2个时序!
若是你没法理解上面这段话,不要紧,请继续向下看。
下面解释 async 关键字作了什么:
下面以原题中的函数 async2
为例,做等价转换
function async2(){ console.log('async2'); return Promise.resolve(); }
这里须要引入 TC39 规范:
规范晦涩难懂,咱们能够看看这篇文章:《「译」更快的 async 函数和 promises》,下面引入其中的一些描述:
简单说,await v 初始化步骤有如下组成:
- 把 v 转成一个 promise(跟在 await 后面的)。
- 绑定处理函数用于后期恢复。
- 暂停 async 函数并返回 implicit_promise 给调用者。
咱们一步步来看,假设 await 后是一个 promise,且最终已完成状态的值是 42。而后,引擎会建立一个新的 promise 而且把 await 后的值做为 resolve 的值。借助标准里的 PromiseResolveThenableJob 这些 promise 会被放到下个周期执行。
结合规范和这篇文章,简单总结一下,对于 await v
:
fulfilled
的 Promise,仍是会新建一个 Promise,并在这个新 Promise 中 resolve(v)
await v
后续的代码的执行相似于传入 then()
中的回调如此,可进一步对原题中的 async1
做等价转换
function async1(){ console.log('async1 start') return new Promise(resolve => resolve(async2())) .then(() => { console.log('async1 end') }); }
至此,咱们根据规范综合以上全部等价转换,将 async/await
所有转换为原生 Promise 实现,其执行顺序在 Chrome71 上与一开始给出的 <转换1> 彻底一致:
function async1(){ console.log('async1 start') return new Promise(resolve => resolve(async2())) .then(() => { console.log('async1 end') }); } function async2(){ console.log('async2'); return Promise.resolve(); } async1(); new Promise((resolve) => { console.log(1) resolve() }).then(() => { console.log(2) }).then(() => { console.log(3) }).then(() => { console.log(4) })
到了这,你是否是感受整个思路变清晰了?不过,仍是不能很好的解释 为何 console.log('async1 end')
在3后面输出,下面将说明其中的缘由。
new Promise(resolve => resolve(thenable))
的处理仔细观察 <转换4> 中的 async1
函数,不难发现 return new Promise(resolve => resolve(async2()))
中,Promise resolve 的是 async2()
,而 async2()
返回了一个状态为 <resolved>: undefined
的 Promsie,Promise 是一个 thenable 对象。
对于 thenable 对象,《ECMAScript 6 入门》中这样描述:
thenable 对象指的是具备then方法的对象,好比下面这个对象
let thenable = { then: function(resolve, reject) { resolve(42); } };
下面须要引入 TC39 规范中对 Promise Resolve Functions 的描述:
o
,若是 o.then
是一个 function
,那么 o
就能够被称为 thenable
对象new Promise(resolve => resolve(thenable))
,即“在 Promise 中 resolve 一个 thenable 对象”,须要先将 thenable 转化为 Promsie,而后当即调用 thenable 的 then 方法,而且 这个过程须要做为一个 job 加入微任务队列,以保证对 then 方法的解析发生在其余上下文代码的解析以后 下面给出示例:
let thenable = { then(resolve, reject) { console.log('in thenable'); resolve(100); } }; new Promise((r) => { console.log('in p0'); r(thenable); }) .then(() => { console.log('thenable ok') }) new Promise((r) => { console.log('in p1'); r(); }) .then(() => { console.log('1') }) .then(() => { console.log('2') }) .then(() => { console.log('3') }) .then(() => { console.log('4') });
执行顺序:
in p0 in p1 in thenable 1 thenable ok 2 3 4
in thenable
后于 in p1
而先于 1
输出,同时 thenable ok
在 1
后输出console.log('1')
thenable.then()
,从而注册了另外一个微任务:console.log('thenable ok')
thenable
的处理须要在一个微任务中完成,从而致使了第一个 Promise 的后续回调被延后了1个时序Promise.prototype.then
,而这时 Promise 若是已是 resolved 状态 ,then 的执行会再一次建立了一个微任务最终结果就是:额外建立了两个Job,表现上就是后续代码被推迟了2个时序
上面围绕规范说了那么多,不知你有没有理解这其中的执行过程。规范是晦涩难懂的,下面咱们结合规范继续对代码做“转换”,让这个过程变得更容易理解一些
对于代码
new Promise((resolve) => { resolve(thenable) })
在执行顺序上等价于(我只敢说“在执行顺序上等价”,由于浏览器的内部实现没法简单的模拟):
new Promise((resolve) => { Promise.resolve().then(() => { thenable.then(resolve) }) })
因此,原题中的 new Promise(resolve => resolve(async2()))
,在执行顺序上等价于:
new Promise((resolve) => { Promise.resolve().then(() => { async2().then(resolve) }) })
综上,给出最终转换:
function async1(){ console.log('async1 start'); const p = async2(); return new Promise((resolve) => { Promise.resolve().then(() => { p.then(resolve) }) }) .then(() => { console.log('async1 end') }); } function async2(){ console.log('async2'); return Promise.resolve(); } async1(); new Promise((resolve) => { console.log(1) resolve() }).then(() => { console.log(2) }).then(() => { console.log(3) }).then(() => { console.log(4) })
OK, 看到这里,你应该理解了为何在 Chrome71 中 async1 end 在 3 后输出了。
不过这还没完呢,认真的你可能已经发现,这里给出的执行顺序在 Chrome73 上不对啊。没错,这是由于 Await 规范更新了……
若是你在 Chrome73 中运行这道题的代码,你会发现,执行顺序与 Chrome71 中不一样,这又是为何?
我来简单说说这个事情的过程:
在 Chrome71 以前的某个版本,nodejs 中有个 bug,这个 bug 的表现就是对 await 进行了激进优化,所谓激进优化,就是没有按照 TC39 规范的要求执行。V8 团队修复了这个 bug。不过,从这个 bug 中 V8 团队获得了启发,发现这个 bug 中的激进优化居然能够带来性能提高,因此向 TC39 提交了改进方案,并会在下个版本中执行这个优化……
上文中提到的译文《「译」更快的 async 函数和 promises》,说的就是这个优化的由来。
文章中的“激进优化”,是指 await v
在语义上将等价于 Promise.resolve(v)
,而再也不是如今的 new Promise(resolve => resolve(v))
,因此在将来的 Chrome73 中,题中的代码可作以下等价转换:
function async1(){ console.log('async1 start'); const p = async2(); return Promise.resolve(p) .then(() => { console.log('async1 end') }); } function async2(){ console.log('async2'); return Promise.resolve(); } async1(); new Promise((resolve) => { console.log(1) resolve() }).then(() => { console.log(2) }).then(() => { console.log(3) }).then(() => { console.log(4) })
执行顺序:
async1 start async2 1 async1 end 2 3 4
有没有以为优化后的版本更容易理解了呢?
Promise.resolve(v)
不等于 new Promise(r => r(v))
,由于若是 v 是一个 Promise 对象,前者会直接返回 v,然后者须要通过一系列的处理(主要是 PromiseResolveThenableJob)setTimeout
所建立的宏任务可视为 第二个宏任务,第一个宏任务是这段程序自己本文从一道你们都熟悉的面试题出发,综合了 TC39 规范和《「译」更快的 async 函数和 promises》这篇文章对浏览器中的 async/await 的执行过程进行了分析,并给出了基于原生 Promise 实现的等价代码。同时,引出了即将进行的性能优化,并简单介绍了该优化的由来。
我要感谢在 SF 社区中与我一同追寻答案的 @xianshenglu,以上所有分析过程的详细讨论在这里:async await 和 promise微任务执行顺序问题
我在偶然中看到了这个问题,因为答案使人难以理解,因此我决定搞个明白,而后便一发不可收拾……
你可能会以为这种在工做中根本不会遇到的代码不必费这么大力气去分析,但经过以上的学习过程我仍是收获了一些知识的,这颠覆了我以前对 async/await
的理解
不得不说,遇到这种问题,仍是得看规范才能搞明白啊……