摘要:問題的關(guān)鍵在于其執(zhí)行過程中的微任務(wù)數(shù)量,下文中我們需要用上述代碼中的方式對(duì)微任務(wù)的執(zhí)行順序進(jìn)行標(biāo)記,以輔助我們理解這其中的執(zhí)行過程。
原文發(fā)布在掘金社區(qū):https://juejin.im/post/5c3cc981f265da616a47e028起源
2019年了,相信大家對(duì) Promise 和 async/await 都不再陌生了。
前幾日,我在社區(qū)讀到了一篇關(guān)于 async/await 執(zhí)行順序的文章《「前端面試題系列1」今日頭條 面試題和思路解析》。文中提到了一道“2017年「今日頭條」的前端面試題”,還有另一篇對(duì)此題的解析文章《8張圖讓你一步步看清 async/await 和 promise 的執(zhí)行順序》,兩文中都對(duì)問題進(jìn)行了分析。不過在我看來,這兩篇文章都沒有把這個(gè)問題說清楚,同時(shí)在評(píng)論區(qū)中也有很多朋友留言表達(dá)了自己的疑惑。
面試題其實(shí)解決這個(gè)問題最關(guān)鍵的是以下兩點(diǎn):
Promise.resolve(v) 不等于 new Promise(resolve => resolve(v))
瀏覽器怎樣處理 new Promise(resolve => resolve(thenable)),即在 Promise 中 resolve 一個(gè) thenable 對(duì)象
國際慣例,先給出面試題和答案:
注:執(zhí)行順序以 Chrome71 為準(zhǔn)
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 async2 promise1 script end promise2 async1 end setTimeout
看完答案后,我與很多人一樣無論如何也不理解 為什么 async1 end 會(huì)晚于promise2 輸出……我的第一反應(yīng)是 我對(duì) await 的理解有偏差,所以我決心要把這個(gè)問題弄明白。
本文主要解釋瀏覽器對(duì) await 的處理,并一步步將原題代碼轉(zhuǎn)換為原生Promsie實(shí)現(xiàn)。
所有執(zhí)行順序以 Chrome71 為準(zhǔn),不討論 Babel 和 Promise 墊片。
第一次發(fā)文,難免有一些不嚴(yán)謹(jǐn)之處,如有錯(cuò)誤,還望大家在評(píng)論區(qū)批評(píng)指正!
基礎(chǔ)在解釋答案之前,你需要先掌握:
Promise 基礎(chǔ)
Promise 執(zhí)行器中的代碼會(huì)被同步調(diào)用
Promise 回調(diào)是基于微任務(wù)的
瀏覽器 eventloop
宏任務(wù)與微任務(wù)的優(yōu)先級(jí)
宏任務(wù)的優(yōu)先級(jí)高于微任務(wù)
每一個(gè)宏任務(wù)執(zhí)行完畢都必須將當(dāng)前的微任務(wù)隊(duì)列清空
第一個(gè) script 標(biāo)簽的代碼是第一個(gè)宏任務(wù)
主要內(nèi)容問題主要涉及以下4點(diǎn):
Promise 的鏈?zhǔn)?then() 是怎樣執(zhí)行的
async 函數(shù)的返回值
await 做了什么
PromiseResolveThenableJob:瀏覽器對(duì) new Promise(resolve => resolve(thenable)) 的處理
下面,讓我們一步步將原題中的代碼轉(zhuǎn)換為更容易理解的等價(jià)代碼。
Promise 的鏈?zhǔn)?then() 是怎樣執(zhí)行的在正式開始之前,我們先來看以下這段代碼:
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()。
為什么要先放出這段代碼?
因?yàn)?async/await可視為 Promise 的語法糖,同樣基于微任務(wù)實(shí)現(xiàn);本題主要糾結(jié)的點(diǎn)在于 await 到底做了什么導(dǎo)致 async1 end 晚于 promise2 輸出。問題的關(guān)鍵在于其執(zhí)行過程中的微任務(wù)數(shù)量,下文中我們需要用上述代碼中的方式對(duì)微任務(wù)的執(zhí)行順序進(jìn)行標(biāo)記,以輔助我們理解這其中的執(zhí)行過程。
分析Promise 多個(gè) then() 鏈?zhǔn)秸{(diào)用,并不是連續(xù)的創(chuàng)建了多個(gè)微任務(wù)并推入微任務(wù)隊(duì)列,因?yàn)?then() 的返回值必然是一個(gè) Promise,而后續(xù)的 then() 是上一步 then() 返回的 Promise 的回調(diào)
傳入 Promise 構(gòu)造器的執(zhí)行器函數(shù)內(nèi)部的同步代碼執(zhí)行到 resolve(),將 Promise 的狀態(tài)改變?yōu)?
第二個(gè) then() 中傳入的回調(diào)函數(shù) console.log("2") 此時(shí)還沒有被推入微任務(wù)隊(duì)列,只有上一個(gè) then() 中的 console.log("1") 執(zhí)行完畢后,console.log("2") 才會(huì)被推入微任務(wù)隊(duì)列
總結(jié)Promise.prototype.then() 會(huì)隱式返回一個(gè)新 Promise
如果 Promise 的狀態(tài)是 pending,那么 then 會(huì)在該 Promise 上注冊一個(gè)回調(diào),當(dāng)其狀態(tài)發(fā)生變化時(shí),對(duì)應(yīng)的回調(diào)將作為一個(gè)微任務(wù)被推入微任務(wù)隊(duì)列
如果 Promise 的狀態(tài)已經(jīng)是 fulfilled 或 rejected,那么 then() 會(huì)立即創(chuàng)建一個(gè)微任務(wù),將傳入的對(duì)應(yīng)的回調(diào)推入微任務(wù)隊(duì)列
為了更好的解析問題,下面我對(duì)原題代碼進(jìn)行一些修改,剔除和主要問題無關(guān)的代碼
<轉(zhuǎn)換1>: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 鏈增加了一個(gè)回調(diào),而最終結(jié)果中 async1 end 在 3 后輸出,而不是在 2 后!
await 一定是做了一些我們不理解的“詭異操作”,令其后續(xù)代碼 console.log("async1 end") 被推遲了2個(gè)時(shí)序。
換句話說,async/await 是 Promise 的語法糖,同樣基于微任務(wù)實(shí)現(xiàn),不可能有其他超出我們理解的東西,所以可以斷定:在 console.log("async1 end") 執(zhí)行前,額外執(zhí)行了2個(gè)微任務(wù),所以導(dǎo)致被推遲2個(gè)時(shí)序!
如果你無法理解上面這段話,沒關(guān)系,請繼續(xù)向下看。
async 函數(shù)的返回值下面解釋 async 關(guān)鍵字做了什么:
被 async 操作符修飾的函數(shù)必然返回一個(gè) Promise
當(dāng) async 函數(shù)返回一個(gè)值時(shí),Promise 的 resolve 方法負(fù)責(zé)傳遞這個(gè)值
當(dāng) async 函數(shù)拋出異常時(shí),Promise 的 reject 方法會(huì)傳遞這個(gè)異常值
下面以原題中的函數(shù) async2 為例,作等價(jià)轉(zhuǎn)換
<轉(zhuǎn)換2>:function async2(){ console.log("async2"); return Promise.resolve(); }await 操作符做了什么
這里需要引入 TC39 規(guī)范:
規(guī)范晦澀難懂,我們可以看看這篇文章:《「譯」更快的 async 函數(shù)和 promises》,下面引入其中的一些描述:
簡單說,await v 初始化步驟有以下組成:
把 v 轉(zhuǎn)成一個(gè) promise(跟在 await 后面的)。
綁定處理函數(shù)用于后期恢復(fù)。
暫停 async 函數(shù)并返回 implicit_promise 給調(diào)用者。
我們一步步來看,假設(shè) await 后是一個(gè) promise,且最終已完成狀態(tài)的值是 42。然后,引擎會(huì)創(chuàng)建一個(gè)新的 promise 并且把 await 后的值作為 resolve 的值。借助標(biāo)準(zhǔn)里的 PromiseResolveThenableJob 這些 promise 會(huì)被放到下個(gè)周期執(zhí)行。
結(jié)合規(guī)范和這篇文章,簡單總結(jié)一下,對(duì)于 await v:
await 后的值 v 會(huì)被轉(zhuǎn)換為 Promise
即使 v 是一個(gè)已經(jīng) fulfilled 的 Promise,還是會(huì)新建一個(gè) Promise,并在這個(gè)新 Promise 中 resolve(v)
await v 后續(xù)的代碼的執(zhí)行類似于傳入 then() 中的回調(diào)
如此,可進(jìn)一步對(duì)原題中的 async1 作等價(jià)轉(zhuǎn)換
<轉(zhuǎn)換3>:function async1(){ console.log("async1 start") return new Promise(resolve => resolve(async2())) .then(() => { console.log("async1 end") }); }
至此,我們根據(jù)規(guī)范綜合以上所有等價(jià)轉(zhuǎn)換,將 async/await 全部轉(zhuǎn)換為原生 Promise 實(shí)現(xiàn),其執(zhí)行順序在 Chrome71 上與一開始給出的 <轉(zhuǎn)換1> 完全一致:
<轉(zhuǎn)換4>: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) })
到了這,你是不是感覺整個(gè)思路變清晰了?不過,還是不能很好的解釋 為什么 console.log("async1 end") 在3后面輸出,下面將說明其中的原因。
PromiseResolveThenableJob:瀏覽器對(duì) new Promise(resolve => resolve(thenable)) 的處理仔細(xì)觀察 <轉(zhuǎn)換4> 中的 async1 函數(shù),不難發(fā)現(xiàn) return new Promise(resolve => resolve(async2())) 中,Promise resolve 的是 async2(),而 async2() 返回了一個(gè)狀態(tài)為
對(duì)于 thenable 對(duì)象,《ECMAScript 6 入門》中這樣描述:
thenable 對(duì)象指的是具有then方法的對(duì)象,比如下面這個(gè)對(duì)象
let thenable = { then: function(resolve, reject) { resolve(42); } };
下面需要引入 TC39 規(guī)范中對(duì) Promise Resolve Functions 的描述:
以及 PromiseResolveThenableJob:
總結(jié):對(duì)于一個(gè)對(duì)象 o,如果 o.then 是一個(gè) function,那么 o 就可以被稱為 thenable 對(duì)象
對(duì)于 new Promise(resolve => resolve(thenable)),即“在 Promise 中 resolve 一個(gè) thenable 對(duì)象”,需要先將 thenable 轉(zhuǎn)化為 Promsie,然后立即調(diào)用 thenable 的 then 方法,并且 這個(gè)過程需要作為一個(gè) job 加入微任務(wù)隊(duì)列,以保證對(duì) then 方法的解析發(fā)生在其他上下文代碼的解析之后
下面給出示例:
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") });
執(zhí)行順序:
in p0 in p1 in thenable 1 thenable ok 2 3 4解析
in thenable 后于 in p1 而先于 1 輸出,同時(shí) thenable ok 在 1 后輸出
在執(zhí)行完同步任務(wù)后,微任務(wù)隊(duì)列中只有2個(gè)微任務(wù):第一個(gè)是 轉(zhuǎn)換thenable為Promise的過程,即 PromiseResolveThenableJob,第二個(gè)是 console.log("1")
在 PromiseResolveThenableJob 執(zhí)行中會(huì)執(zhí)行 thenable.then(),從而注冊了另一個(gè)微任務(wù):console.log("thenable ok")
正是由于規(guī)范中對(duì) thenable 的處理需要在一個(gè)微任務(wù)中完成,從而導(dǎo)致了第一個(gè) Promise 的后續(xù)回調(diào)被延后了1個(gè)時(shí)序
如果在 Promise 中 resolve 一個(gè) Promise 實(shí)例呢?由于 Promise 實(shí)例是一個(gè)對(duì)象,其原型上有 then 方法,所以這也是一個(gè) thenable 對(duì)象。
同樣的,瀏覽器會(huì)創(chuàng)建一個(gè) PromiseResolveThenableJob 去處理這個(gè) Promise 實(shí)例,這是一個(gè)微任務(wù)。
在 PromiseResolveThenableJob 執(zhí)行中,執(zhí)行了 Promise.prototype.then,而這時(shí) Promise 如果已經(jīng)是 resolved 狀態(tài) ,then 的執(zhí)行會(huì)再一次創(chuàng)建了一個(gè)微任務(wù)
最終結(jié)果就是:額外創(chuàng)建了兩個(gè)Job,表現(xiàn)上就是后續(xù)代碼被推遲了2個(gè)時(shí)序
最終轉(zhuǎn)換上面圍繞規(guī)范說了那么多,不知你有沒有理解這其中的執(zhí)行過程。規(guī)范是晦澀難懂的,下面我們結(jié)合規(guī)范繼續(xù)對(duì)代碼作“轉(zhuǎn)換”,讓這個(gè)過程變得更容易理解一些
對(duì)于代碼
new Promise((resolve) => { resolve(thenable) })
在執(zhí)行順序上等價(jià)于(我只敢說“在執(zhí)行順序上等價(jià)”,因?yàn)闉g覽器的內(nèi)部實(shí)現(xiàn)無法簡單的模擬):
new Promise((resolve) => { Promise.resolve().then(() => { thenable.then(resolve) }) })
所以,原題中的 new Promise(resolve => resolve(async2())),在執(zhí)行順序上等價(jià)于:
new Promise((resolve) => { Promise.resolve().then(() => { async2().then(resolve) }) })
綜上,給出最終轉(zhuǎn)換:
<轉(zhuǎn)換-END>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, 看到這里,你應(yīng)該理解了為什么在 Chrome71 中 async1 end 在 3 后輸出了。
不過這還沒完呢,認(rèn)真的你可能已經(jīng)發(fā)現(xiàn),這里給出的執(zhí)行順序在 Chrome73 上不對(duì)啊。沒錯(cuò),這是因?yàn)?Await 規(guī)范更新了……
Await 規(guī)范的更新如果你在 Chrome73 中運(yùn)行這道題的代碼,你會(huì)發(fā)現(xiàn),執(zhí)行順序與 Chrome71 中不同,這又是為什么?
我來簡單說說這個(gè)事情的過程:
在 Chrome71 之前的某個(gè)版本,nodejs 中有個(gè) bug,這個(gè) bug 的表現(xiàn)就是對(duì) await 進(jìn)行了激進(jìn)優(yōu)化,所謂激進(jìn)優(yōu)化,就是沒有按照 TC39 規(guī)范的要求執(zhí)行。V8 團(tuán)隊(duì)修復(fù)了這個(gè) bug。不過,從這個(gè) bug 中 V8 團(tuán)隊(duì)得到了啟發(fā),發(fā)現(xiàn)這個(gè) bug 中的激進(jìn)優(yōu)化竟然可以帶來性能提升,所以向 TC39 提交了改進(jìn)方案,并會(huì)在下個(gè)版本中執(zhí)行這個(gè)優(yōu)化……
上文中提到的譯文《「譯」更快的 async 函數(shù)和 promises》,說的就是這個(gè)優(yōu)化的由來。
激進(jìn)優(yōu)化文章中的“激進(jìn)優(yōu)化”,是指 await v 在語義上將等價(jià)于 Promise.resolve(v),而不再是現(xiàn)在的 new Promise(resolve => resolve(v)),所以在未來的 Chrome73 中,題中的代碼可做如下等價(jià)轉(zhuǎn)換:
<轉(zhuǎn)換-優(yōu)化版本>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) })
執(zhí)行順序:
async1 start async2 1 async1 end 2 3 4
有沒有覺得優(yōu)化后的版本更容易理解了呢?
還需要補(bǔ)充的要點(diǎn)Promise.resolve(v) 不等于 new Promise(r => r(v)),因?yàn)槿绻?v 是一個(gè) Promise 對(duì)象,前者會(huì)直接返回 v,而后者需要經(jīng)過一系列的處理(主要是 PromiseResolveThenableJob)
宏任務(wù)的優(yōu)先級(jí)是高于微任務(wù)的,而原題中的 setTimeout 所創(chuàng)建的宏任務(wù)可視為 第二個(gè)宏任務(wù),第一個(gè)宏任務(wù)是這段程序本身
總結(jié)本文從一道大家都熟悉的面試題出發(fā),綜合了 TC39 規(guī)范和《「譯」更快的 async 函數(shù)和 promises》這篇文章對(duì)瀏覽器中的 async/await 的執(zhí)行過程進(jìn)行了分析,并給出了基于原生 Promise 實(shí)現(xiàn)的等價(jià)代碼。同時(shí),引出了即將進(jìn)行的性能優(yōu)化,并簡單介紹了該優(yōu)化的由來。
我要感謝在 SF 社區(qū)中與我一同追尋答案的 @xianshenglu,以上全部分析過程的詳細(xì)討論在這里:async await 和 promise微任務(wù)執(zhí)行順序問題
最后:我在偶然中看到了這個(gè)問題,由于答案令人難以理解,所以我決定搞個(gè)明白,然后便一發(fā)不可收拾……
你可能會(huì)覺得這種在工作中根本不會(huì)遇到的代碼沒必要費(fèi)這么大力氣去分析,但通過以上的學(xué)習(xí)過程我還是收獲了一些知識(shí)的,這顛覆了我之前對(duì) async/await 的理解
不得不說,遇到這種問題,還是得看規(guī)范才能搞明白啊……
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/101219.html
摘要:函數(shù)會(huì)在之后的某個(gè)時(shí)刻觸發(fā)事件定時(shí)器。事件循環(huán)中的這樣一次遍歷被稱為一個(gè)。執(zhí)行完畢并出棧。當(dāng)定時(shí)器過期,宿主環(huán)境會(huì)把回調(diào)函數(shù)添加至事件循環(huán)隊(duì)列中,然后,在未來的某個(gè)取出并執(zhí)行該事件。 原文請查閱這里,略有改動(dòng)。 本系列持續(xù)更新中,Github 地址請查閱這里。 這是 JavaScript 工作原理的第四章。 現(xiàn)在,我們將會(huì)通過回顧單線程環(huán)境下編程的弊端及如何克服這些困難以創(chuàng)建令人驚嘆...
摘要:前言對(duì)于這門語言,其實(shí)我更喜歡稱它為,從一開始我們就已經(jīng)涉及到異步編程,但是多數(shù)開發(fā)者從來沒有認(rèn)真思考過自己程序中的異步,到底是怎么實(shí)現(xiàn)的,以及為什么會(huì)出現(xiàn)。 前言 對(duì)于JavaScript這門語言,其實(shí)我更喜歡稱它為ECMAScript,從一開始我們就已經(jīng)涉及到異步編程,但是多數(shù)JavaScript開發(fā)者從來沒有認(rèn)真思考過自己程序中的異步,到底是怎么實(shí)現(xiàn)的,以及為什么會(huì)出現(xiàn)。但是由于...
摘要:從開始,就在引入新功能,來幫助更簡單的方法來處理異步編程,幫助我們遠(yuǎn)離回調(diào)地獄。而則是為了更簡潔的使用而提出的語法,相比這種的實(shí)現(xiàn)方式,更為專注,生來就是為了處理異步編程。 從Promise開始,JavaScript就在引入新功能,來幫助更簡單的方法來處理異步編程,幫助我們遠(yuǎn)離回調(diào)地獄。 Promise是下邊要講的Generator/yield與async/await的基礎(chǔ),希望你已...
摘要:事件循環(huán)從回調(diào)隊(duì)列中獲取并將其推送到調(diào)用堆棧。如何工作請注意,不會(huì)自動(dòng)將您的回調(diào)函數(shù)放到事件循環(huán)隊(duì)列中。它設(shè)置了一個(gè)計(jì)時(shí)器,當(dāng)計(jì)時(shí)器到期時(shí),環(huán)境將您的回調(diào)函數(shù)放入事件循環(huán)中,以便將來的某個(gè)事件會(huì)將其選中并執(zhí)行它。 我們將通過回顧第一篇文章中單線程編程的缺點(diǎn),然后在討論如何克服它們來構(gòu)建令人驚嘆的JavaScript UI。在文章結(jié)尾處,我們將分享5個(gè)關(guān)于如何使用async / awai...
閱讀 3412·2023-04-26 01:40
閱讀 3107·2021-11-24 09:39
閱讀 1417·2021-10-27 14:19
閱讀 2662·2021-10-12 10:11
閱讀 1329·2021-09-26 09:47
閱讀 1868·2021-09-22 15:21
閱讀 2776·2021-09-06 15:00
閱讀 913·2021-08-10 09:44