成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

令人費(fèi)解的 async/await 執(zhí)行順序

WilsonLiu95 / 3474人閱讀

摘要:問題的關(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)?: undefined, 然后 then 中傳入的回調(diào)函數(shù) console.log("1") 作為一個(gè)微任務(wù)被推入微任務(wù)隊(duì)列

第二個(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 和一些同步代碼,然后為 Promisethen 鏈增加了一個(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)為 : undefined 的 Promsie,Promise 是一個(gè) thenable 對(duì)象。

對(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 ok1 后輸出

在執(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

相關(guān)文章

  • JavaScript 工作原理之四-事件循環(huán)及異步編程出現(xiàn)和 5 種更好 async/await

    摘要:函數(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)建令人驚嘆...

    maochunguang 評(píng)論0 收藏0
  • 你不知道JavaScript :Promise 與 Async/Await

    摘要:前言對(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)。但是由于...

    wmui 評(píng)論0 收藏0
  • JavaScript異步編程:Generator與Async

    摘要:從開始,就在引入新功能,來幫助更簡單的方法來處理異步編程,幫助我們遠(yuǎn)離回調(diào)地獄。而則是為了更簡潔的使用而提出的語法,相比這種的實(shí)現(xiàn)方式,更為專注,生來就是為了處理異步編程。 從Promise開始,JavaScript就在引入新功能,來幫助更簡單的方法來處理異步編程,幫助我們遠(yuǎn)離回調(diào)地獄。 Promise是下邊要講的Generator/yield與async/await的基礎(chǔ),希望你已...

    leon 評(píng)論0 收藏0
  • JavaScript工作原理(四):事件循環(huán),異步編程興起以及5招async/await實(shí)踐

    摘要:事件循環(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...

    piglei 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<