async/await javascript ES 2017 ECMAScript 2017 async await Promise

Async/await hell

傅思允 Siyun Fu 2021/12/30 15:56:44
1365

相信你一定有看過上面這張圖,Promise將我們從如上的callback hell中解救出來,而async/await又將我們從易讀性較低的promise chain中解救出來,然而濫用async/await的結果卻衍生出了新的async/await hell。而什麼是async/await hell?

以準備早餐為例,你可以一個人處完下列的事情:

  • 煮咖啡
  • 烤土司
  • 煎培根
  • 炒蛋

寫成程式碼如下所示:

async function makeCoffee() {
  console.log("煮水中...");
  await delay(5000);
  console.log("咖啡煮好了");
  return '倒咖啡'
}

async function toastBread() {
  console.log("烤麵包...");
  await delay(3000);
  console.log("麵包跳起來");
  return '麵包盛盤'
}

async function fryBacon() {
  console.log("熱鍋子...");
  await delay(5000);
  console.log("煎培根...");
  await delay(5000);
  console.log("培根完成了");
  return "培根盛盤";
}


async function fryEgg() {
  console.log("熱鍋子...");
  await delay(5000);
  console.log("炒蛋...");
  await delay(5000);
  console.log("炒蛋完成了");
  return '蛋盛盤'
} 

 

準備早餐的流程如下

async function prepareBrekfastSync() {
  const startTime = new Date().getTime();

  const hotCoffee = await makeCoffee();
  const friedBacon = await fryBacon();
  const friedEgg = await fryEgg();
  const bakedToast = await toastBread();

  console.log(hotCoffee)
  console.log(friedBacon)
  console.log(friedEgg)
  console.log(bakedToast)

  const endTime = new Date().getTime();
  console.log(`總共花費${(endTime - startTime) / 1000}分`);
}

整個早餐做下來花了你20幾分鐘,這是各件事所需時間加總下來所花費的總時間,因為你是依序地處理所有事情,等待一件事完成後才去執行下一件事,當早餐完成時,咖啡也早已涼掉。

然而你仔細地審視整個做早餐的流程,你會發現有幾件事其實是毫不相干的,並不需要去空等:你不需要等在麵包機前等麵包烤完、也不需要癡癡的等待咖啡機做好你的咖啡,上面的範例就形成了所謂的async await hell。

如何去改善整個流程?

 

避開async/await hell

你會發現你可以平行地進行每一件事,在咖啡機在煮咖啡的空閒時先去烤麵包、熱鍋子,然後你可以在最後時再去等待(await)每件事完成,如下:

async function prepareBrekfastAsyncDiff() {
  const startTime = new Date().getTime();

  const coffeeTask = makeCoffee();
  const eggTask = fryEgg();
  const baconTask = fryBacon();
  const toastTask = toastBread();

  const hotCoffee = await coffeeTask;
  console.log(hotCoffee);
  const friedBacon = await baconTask;
  console.log(friedBacon);
  const friedEgg = await eggTask;
  console.log(friedEgg);
  const bakedToast = await toastTask;
  console.log(bakedToast);

  const endTime = new Date().getTime();
  console.log(`總共花費${(endTime - startTime) / 1000}分`);
}

 

如果要更精簡可以使用Promise.all,所花費的時間是差不多的

async function prepareBrekfastPromiseAll() {
  const startTime = new Date().getTime();

  const meals = await Promise.all([
    makeCoffee(),
    fryBacon(),
    fryEgg(),
    toastBread(),
  ]);

  meals.forEach((meal) => {
    console.log(meal);
  });

  const endTime = new Date().getTime();
  console.log(`總共花費${(endTime - startTime) / 1000}分`);
}

你在開始第一件事後緊接著開始著手其他事情,而在等待所有事情完成後,再一一的去處理,做早餐所花費的時間整整少了一半,這樣看起來已經很完美了。

進階

一般來說做到上面就差不多了,也已經成功地避開async/await hell。

但如果你很龜毛,仔細審視整個流程

煮水中...
熱鍋子...
熱鍋子...
烤麵包...
麵包跳起來
咖啡煮好了
煎培根...
炒蛋...
炒蛋完成了
培根完成了
倒咖啡
培根盛盤
蛋盛盤
麵包盛盤
總共花費10.033分

 

你可能會發現有一些餐點早就已經完成卻還要等其他餐點完成才能做最後的動作。當所有餐點完成一一盛盤上桌、這時候你鍋子裡的蛋可能已經過熟。你可能會希望先完成的先處理,達成真正的平行(parallel)處理。

async function prepareBrekfastParallel() {
  const startTime = new Date().getTime();
  await Promise.all([
    (async () => console.log(await makeCoffee()))(),
    (async () => console.log(await fryBacon()))(),
    (async () => console.log(await fryEgg()))(),
    (async () => console.log(await toastBread()))(),
  ]);

  const endTime = new Date().getTime();
  console.log(`總共花費${(endTime - startTime) / 1000}分`);
}

 

你也可以使用Promise.race,先將任務放在任務列中,完成先處理並剔除,直到任務列中沒有任務。

async function prepareBrekfastPromiseRace() {
  const startTime = new Date().getTime();

  const taskList = [makeCoffee(), fryEgg(), fryBacon(), toastBread()];
  while (taskList.length > 0) {
    const indexTask = taskList.map((p, index) => p.then(() => index));
    const toRemoveIndex = Promise.race(indexTask);
    const result = taskList.splice(await toRemoveIndex, 1)[0];
    console.log(await result);
  }
  
  const endTime = new Date().getTime();
  console.log(`總共花費${(endTime - startTime) / 1000}分`);
}

 

範例碼:https://playcode.io/848552/

 

傅思允 Siyun Fu