精讀《async/await 是把雙刃劍》

本週精讀內容是 《逃離 async/await 地獄》html

1 引言

終於,async/await 也被吐槽了。Aditya Agarwal 認爲 async/await 語法讓咱們陷入了新的麻煩之中。前端

其實,筆者也早就以爲哪兒不對勁了,終於有我的把實話說了出來,async/await 可能會帶來麻煩。jquery

2 概述

下面是隨處可見的現代化前端代碼:git

(async () => {
  const pizzaData = await getPizzaData(); // async call
  const drinkData = await getDrinkData(); // async call
  const chosenPizza = choosePizza(); // sync call
  const chosenDrink = chooseDrink(); // sync call
  await addPizzaToCart(chosenPizza); // async call
  await addDrinkToCart(chosenDrink); // async call
  orderItems(); // async call
})();
複製代碼

await 語法自己沒有問題,有時候多是使用者用錯了。當 pizzaDatadrinkData 之間沒有依賴時,順序的 await 會最多讓執行時間增長一倍的 getPizzaData 函數時間,由於 getPizzaDatagetDrinkData 應該並行執行。github

回到咱們吐槽的回調地獄,雖然代碼比較醜,帶起碼兩行回調代碼並不會帶來阻塞。typescript

看來語法的簡化,帶來了性能問題,並且直接影響到用戶體驗,是否是值得咱們反思一下?redux

正確的作法應該是先同時執行函數,再 await 返回值,這樣能夠並行執行異步函數:api

(async () => {
  const pizzaPromise = selectPizza();
  const drinkPromise = selectDrink();
  await pizzaPromise;
  await drinkPromise;
  orderItems(); // async call
})();
複製代碼

或者使用 Promise.all 可讓代碼更可讀:框架

(async () => {
  Promise.all([selectPizza(), selectDrink()]).then(orderItems); // async call
})();
複製代碼

看來不要隨意的 await,它極可能讓你代碼性能下降。異步

3 精讀

仔細思考爲何 async/await 會被濫用,筆者認爲是它的功能比較反直覺致使的。

首先 async/await 真的是語法糖,功能也僅是讓代碼寫的舒服一些。先不看它的語法或者特性,僅從語法糖三個字,就能看出它必定是侷限了某些能力。

舉個例子,咱們利用 html 標籤封裝了一個組件,帶來了便利性的同時,其功能必定是 html 的子集。又好比,某個輪子哥以爲某個組件 api 太複雜,因而基於它封裝了一個語法糖,咱們多半能夠認爲這個便捷性是犧牲了部分功能換來的。

功能完整度與使用便利度一直是相互博弈的,不少框架思想的不一樣開源版本,幾乎都是把功能完整度與便利度按照不一樣比例混合的結果。

那麼回到 async/await 它的解決的問題是回調地獄帶來的災難:

a(() => {
  b(() => {
    c();
  });
});
複製代碼

爲了減小嵌套結構太多對大腦形成的衝擊,async/await 決定這麼寫:

await a();
await b();
await c();
複製代碼

雖然層級上一致了,但邏輯上仍是嵌套關係,這不是另外一個程度上增長了大腦負擔嗎?並且這個轉換仍是隱形的,因此許多時候,咱們傾向於忽略它,因此形成了語法糖的濫用。

理解語法糖

雖然要正確理解 async/await 的真實效果比較反人類,但爲了清爽的代碼結構,以及防止寫出低性能的代碼,仍是挺有必要認真理解 async/await 帶來的改變。

首先 async/await 只能實現一部分回調支持的功能,也就是僅能方便應對層層嵌套的場景。其餘場景,就要動一些腦子了。

好比兩對回調:

a(() => {
  b();
});

c(() => {
  d();
});
複製代碼

若是寫成下面的方式,雖然必定能保證功能一致,但變成了最低效的執行方式:

await a();
await b();
await c();
await d();
複製代碼

由於翻譯成回調,就變成了:

a(() => {
  b(() => {
    c(() => {
      d();
    });
  });
});
複製代碼

然而咱們發現,原始代碼中,函數 c 能夠與 a 同時執行,但 async/await 語法會讓咱們傾向於在 b 執行完後,再執行 c

因此當咱們意識到這一點,能夠優化一下性能:

const resA = a();
const resC = c();

await resA;
b();
await resC;
d();
複製代碼

但其實這個邏輯也沒法達到回調的效果,雖然 ac 同時執行了,但 d 本來只要等待 c 執行完,如今若是 a 執行時間比 c 長,就變成了:

a(() => {
  d();
});
複製代碼

看來只有徹底隔離成兩個函數:

(async () => {
  await a();
  b();
})();

(async () => { await c(); d(); })(); 複製代碼

或者利用 Promise.all:

async function ab() {
  await a();
  b();
}

async function cd() {
  await c();
  d();
}

Promise.all([ab(), cd()]);
複製代碼

這就是我想表達的可怕之處。回調方式這麼簡單的過程式代碼,換成 async/await 竟然寫完還要反思一下,再反推着去優化性能,這簡直比回調地獄還要可怕。

並且大部分場景代碼是很是複雜的,同步與 await 混雜在一塊兒,想捋清楚其中的脈絡,並正確優化性能每每是很困難的。可是咱們爲何要本身挖坑再填坑呢?不少時候還會致使忘了填。

原文做者給出了 Promise.all 的方式簡化邏輯,但筆者認爲,不要一昧追求 async/await 語法,在必要狀況下適當使用回調,是能夠增長代碼可讀性的。

4 總結

async/await 回調地獄提醒着咱們,不要過渡依賴新特性,不然可能帶來的代碼執行效率的降低,進而影響到用戶體驗。同時,筆者認爲,也不要過渡利用新特性修復新特性帶來的問題,這樣反而致使代碼可讀性降低。

當我翻開 redux 剛火起來那段時期的老代碼,看到了許多過渡抽象、爲了用而用的代碼,硬是把兩行代碼能寫完的邏輯,拆到了 3 個文件,分散在 6 行不一樣位置,我只好用字符串搜索的方式查找線索,最後發現這個抽象代碼整個項目僅用了一次。

寫出這種代碼的可能性只有一個,就是在精神麻木的狀況下,一口氣喝完了 redux 提供的所有雞湯。

就像 async/await 地獄同樣,看到這種 redux 代碼,我以爲遠不如所謂沒跟上時代的老前端寫出的 jquery 代碼。

決定代碼質量的是思惟,而非框架或語法,async/await 雖好,但也要適度哦。

5 更多討論

討論地址是:精讀《逃離 async/await 地獄》 · Issue #82 · dt-fe/weekly

若是你想參與討論,請點擊這裏,每週都有新的主題,週末或週一發佈。

相關文章
相關標籤/搜索