溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

詳解ES6之a(chǎn)sync+await 同步/異步方案

發(fā)布時(shí)間:2020-09-03 10:55:57 來(lái)源:腳本之家 閱讀:269 作者:Jeremy_young 欄目:web開發(fā)

異步編程一直是JavaScript 編程的重大事項(xiàng)。關(guān)于異步方案, ES6 先是出現(xiàn)了 基于狀態(tài)管理的 Promise,然后出現(xiàn)了 Generator 函數(shù) + co 函數(shù),緊接著又出現(xiàn)了 ES7 的 async + await 方案。

本文力求以最簡(jiǎn)明的方式來(lái)疏通 async + await。

異步編程的幾個(gè)場(chǎng)景

先從一個(gè)常見問題開始:一個(gè)for 循環(huán)中,如何異步的打印迭代順序?

我們很容易想到用閉包,或者 ES6 規(guī)定的 let 塊級(jí)作用域來(lái)回答這個(gè)問題。

for (let val of [1, 2, 3, 4]) {
  setTimeout(() => console.log(val),100);
}
// => 預(yù)期結(jié)果依次為:1, 2, 3, 4

這里描述的是一個(gè)均勻發(fā)生的的異步,它們被依次按既定的順序排在異步隊(duì)列中等待執(zhí)行。

如果異步不是均勻發(fā)生的,那么它們被注冊(cè)在異步隊(duì)列中的順序就是亂序的。

for (let val of [1, 2, 3, 4]) {
  setTimeout(() => console.log(val), 100 * Math.random());
}
// => 實(shí)際結(jié)果是隨機(jī)的,依次為:4, 2, 3, 1

返回的結(jié)果是亂序不可控的,這本來(lái)就是最為真實(shí)的異步。但另一種情況是,在循環(huán)中,如果希望前一個(gè)異步執(zhí)行完畢、后一個(gè)異步再執(zhí)行,該怎么辦?

for (let val of ['a', 'b', 'c', 'd']) {
  // a 執(zhí)行完后,進(jìn)入下一個(gè)循環(huán)
  // 執(zhí)行 b,依此類推
}

這不就是多個(gè)異步 “串行” 嗎!

在回調(diào) callback 嵌套異步操作、再回調(diào)的方式,不就解決了這個(gè)問題!或者,使用 Promise + then() 層層嵌套同樣也能解決問題。但是,如果硬是要將這種嵌套的方式寫在循環(huán)中,還恐怕還需費(fèi)一番周折。試問,有更好的辦法嗎?

異步同步化方案

試想,如果要去將一批數(shù)據(jù)發(fā)送到服務(wù)器,只有前一批發(fā)送成功(即服務(wù)器返回成功的響應(yīng)),才開始下一批數(shù)據(jù)的發(fā)送,否則終止發(fā)送。這就是一個(gè)典型的 “for 循環(huán)中存在相互依賴的異步操作” 的例子。

明顯,這種 “串行” 的異步,實(shí)質(zhì)上可以當(dāng)成同步。它和亂序的異步比較起來(lái),花費(fèi)了更多的時(shí)間。按理說,我們希望程序異步執(zhí)行,就是為了 “跳過” 阻塞,較少時(shí)間花銷。但與之相反的是,如果需要一系列的異步 “串行”,我們應(yīng)該怎樣很好的進(jìn)行編程?

對(duì)于這個(gè) “串行” 異步,有了 ES6 就非常容易的解決了這個(gè)問題。

async function task () {
  for (let val of [1, 2, 3, 4]) {
    // await 是要等待響應(yīng)的
    let result = await send(val);
    if (!result) {
      break;
    }
  }
}
task();

從字面上看,就是本次循環(huán),等有了結(jié)果,再進(jìn)行下一次循環(huán)。因此,循環(huán)每執(zhí)行一次就會(huì)被暫停(“卡住”)一次,直到循環(huán)結(jié)束。這種編碼實(shí)現(xiàn),很好的消除了層層嵌套的 “回調(diào)地獄” 問題,降低了認(rèn)知難度。

這就是異步問題同步化的方案。關(guān)于這個(gè)方案,如果說 Promise 主要解決的是異步回調(diào)問題,那么 async + await 主要解決的就是將異步問題同步化,降低異步編程的認(rèn)知負(fù)擔(dān)。

async + await “外異內(nèi)同”

早先接觸這套 API 時(shí),看著繁瑣的文檔,一知半解的認(rèn)為 async + await 主要用來(lái)解決異步問題同步化的。

其實(shí)不然。從上面的例子看到:async 關(guān)鍵字聲明了一個(gè) 異步函數(shù),這個(gè) 異步函數(shù) 體內(nèi)有一行 await 語(yǔ)句,它告示了該行為同步執(zhí)行,并且與上下相鄰的代碼是依次逐行執(zhí)行的。

將這個(gè)形式化的東西再翻譯一下,就是:

1、async 函數(shù)執(zhí)行后,總是返回了一個(gè) promise 對(duì)象
2、await 所在的那一行語(yǔ)句是同步的

其中,1 說明了從外部看,task 方法執(zhí)行后返回一個(gè) Promise 對(duì)象,正因?yàn)樗祷氐氖?Promise,所以可以理解task 是一個(gè)異步方法。毫無(wú)疑問它是這樣用的:

task().then((val) => {alert(val)})
   .then((val) => {alert(val)})

2 說明了在 task 函數(shù)內(nèi)部,異步已經(jīng)被 “削” 成了同步。整個(gè)就是一個(gè)執(zhí)行稍微耗時(shí)的函數(shù)而已。

綜合 1、2,從形式上看,就是 “task 整體是一個(gè)異步函數(shù),內(nèi)部整個(gè)是同步的”,簡(jiǎn)稱“外異內(nèi)同”。

整體是一個(gè)異步函數(shù) 不難理解。在實(shí)現(xiàn)上,我們不妨逆向一下,語(yǔ)言層面讓async關(guān)鍵字調(diào)用時(shí),在函數(shù)執(zhí)行的末尾強(qiáng)制增加一個(gè)promise 反回:

async fn () {
  let result;
  // ...
  //末尾返回 promise
  return isPromise(result)? 
      result : Promise.resolve(undefined);
}

內(nèi)部是同步的 是怎么做到的?實(shí)際上 await 調(diào)用,是讓后邊的語(yǔ)句(函數(shù))做了一個(gè)遞歸執(zhí)行,直到獲取到結(jié)果并使其 狀態(tài) 變更,才會(huì) resolve 掉,而只有 resolve 掉,await 那一行代碼才算執(zhí)行完,才繼續(xù)往下一行執(zhí)行。所以,盡管外部是一個(gè)大大的 for 循環(huán),但是整個(gè) for 循環(huán)是依次串行的。

因此,僅從上述框架的外觀出發(fā),就不難理解 async + await 的意義。使用起來(lái)也就這么簡(jiǎn)單,反而 Promise 是一個(gè)必須掌握的基礎(chǔ)件。

秉承本次《重讀 ES6》系列的原則,不過多追求理解細(xì)節(jié)和具體實(shí)現(xiàn)過程。我們繼續(xù)鞏固一下這個(gè) “形式化” 的理解。

async + await 的進(jìn)一步理解

有這樣的一個(gè)異步操作 longTimeTask,已經(jīng)用 Promise 進(jìn)行了包裝。借助該函數(shù)進(jìn)行一系列驗(yàn)證。

const longTimeTask = function (time) {
 return new Promise((resolve, reject) => {
  setTimeout(()=>{
   console.log(`等了 ${time||'xx'} 年,終于回信了`);
   resolve({'msg': 'task done'});
  }, time||1000)
 })
}

async 函數(shù)的執(zhí)行情況

如果,想查看 async exec1 函數(shù)的返回結(jié)果,以及 await 命令的執(zhí)行結(jié)果:

const exec1 = async function () {
 let result = await longTimeTask();
 console.log('result after long time ===>', result);
}
// 查看函數(shù)內(nèi)部執(zhí)行順序
exec1();
// => 等了 xx 年,終于回信了
// => result after long time ===> Object {msg: "task done"}

//查看函數(shù)總體返回值
console.log(exec1());
// => Promise {[[PromiseStatus]]: "pending",...}
// => 同上

以上 2 步執(zhí)行,清晰的證明了 exec1 函數(shù)體內(nèi)是同步、逐行逐行執(zhí)行的,即先執(zhí)行完異步操作,然后進(jìn)行 console.log() 打印。而 exec1() 的執(zhí)行結(jié)果就直接是一個(gè) Promise,因?yàn)樗钕葧?huì)蹦出來(lái)一串 Promise ...,然后才是 exec1 函數(shù)的內(nèi)部執(zhí)行日志。

因此,所有驗(yàn)證,完全符合 整體是一個(gè)異步函數(shù),內(nèi)部整個(gè)是同步的 的總結(jié)。

await 如何執(zhí)行其后語(yǔ)句?

回到 await ,看看它是如何執(zhí)行其后邊的語(yǔ)句的。假設(shè):讓 longTimeTask() 后邊直接帶 then() 回調(diào),分兩種情況:

1)then() 中不再返回任何東西
2) then() 中繼續(xù)手動(dòng)返回另一個(gè) promise

const exec2 = async function () {
 let result = await longTimeTask().then((res) => {
  console.log('then ===>', res.msg);
  res.msg = `${res.msg} then refrash message`;
  // 注釋掉這條 return 或 手動(dòng)返回一個(gè) promise
  return Promise.resolve(res);
 });
 console.log('result after await ===>', result.msg);
}
exec2();
// => 情況一 TypeError: Cannot read property 'msg' of undefined
// => 情況二 正常

首先,longTimeTask() 加上再多得 then() 回調(diào),也不過是放在了它的回調(diào)列隊(duì) queue 里了。也就是說,await 命令之后始終是一條 表達(dá)式語(yǔ)句,只不過上述代碼書寫方式比較讓人迷惑。(比較好的實(shí)踐建議是,將 longTimeTask 方法身后的 then() 移入 longTimeTask 函數(shù)體封裝起來(lái))

其次,手動(dòng)返回另一個(gè) promise 和什么也不返回,關(guān)系到 longTimeTask() 方法最終 resolve 出去的內(nèi)容不一樣。換句話說,await 命令會(huì)提取其后邊的promise 的 resolve 結(jié)果,進(jìn)而直接導(dǎo)致 result 的不同。

值得強(qiáng)調(diào)的是,await 命令只認(rèn) resolve 結(jié)果,對(duì) reject 結(jié)果報(bào)錯(cuò)。不妨用以下的 return 語(yǔ)句替換上述 return 進(jìn)行驗(yàn)證。

return Promise.reject(res);

最后

其實(shí),關(guān)于異步編程還有很多可以梳理的,比如跨模塊的異步編程、異步的單元測(cè)試、異步的錯(cuò)誤處理以及什么是好的實(shí)踐。All in all, 限于篇幅,不在此匯總了。最后,async + await 確實(shí)是一個(gè)很優(yōu)雅的方案。

以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持億速云。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI