為什麼 async/await 不僅僅是句法糖

前端小智發表於2023-04-17
微信搜尋 【大遷世界】, 我會第一時間和你分享前端行業趨勢,學習途徑等等。
本文 GitHub https://github.com/qq449245884/xiaozhi 已收錄,有一線大廠面試完整考點、資料以及我的系列文章。

開篇觀點,async/await 不僅僅是 Promise 上面的語法糖,因為 async/await 確實提供了切實的好處。

  • async/await 讓非同步程式碼變成同步的方式,從而使程式碼更具表現力和可讀性。
  • async/await 統一了非同步程式設計的經驗;以及提供了更好的錯誤堆疊跟蹤。

關於 JS 中非同步程式設計的一點歷史

非同步程式設計在 JavaScript 中很常見。每當我們需要進行網路服務呼叫、檔案訪問或資料庫操作時,儘管語言是單執行緒的,但非同步性是我們防止使用者介面被阻塞的方法。

在 ES6 之前,回撥是猿們處理非同步程式設計的方式。我們表達時間依賴性(即非同步操作的執行順序)的唯一方法是將一個回撥巢狀在另一個回撥中,這導致了所謂的回撥地獄

Es6 中引入了 Promise,它是一個用於非同步操作的一流物件,我們可以輕鬆地傳遞、組合、聚合和應用轉換。時間上的依賴性透過 then方法鏈乾淨地表達出來。

有了 Promise 這個強大的夥伴,聽起來非同步程式設計在 JS 中是一個已經解決的問題,對嗎?

恩,還沒有,因為有時候 Promise 的級別太低了,不太適合使用。

有時 Promise 的級別太低,不適合使用

儘管出現了 Promise,但在 JS 中仍然需要一個更高階別的語言結構來進行非同步程式設計。

我們來看個例子, 假設我們需要某個函式在某個時間間隔輪詢一個API。當達到最大重試次數時,它就會解析為 null

下面是 Promise 的一種解決方案:

let count = 0;

function apiCall() {
  return new Promise((resolve) =>
    // a在第6次重試時,它被解析為 "value"。
    count++ === 5 ? resolve('value') : resolve(null)
  );
}

function sleep(interval) {
  return new Promise((resolve) => setTimeout(resolve, interval));
}

function poll(retry, interval) {
  return new Promise((resolve) => {
    // 為了簡潔起見,跳過錯誤處理

    if (retry === 0) resolve(null);
    apiCall().then((val) => {
      if (val !== null) resolve(val);
      else {
        sleep(interval).then(() => {
          resolve(poll(retry - 1, interval));
        });
      }
    });
  });
}

poll(6, 1000).then(console.log); // 'value'

這種解決方案的直觀性和可讀性取決於人們對Promise的熟悉程度,以及 Promise.resolve 如何 "平鋪" Promise 和遞迴。對我來說,這不是寫這樣一個函式的最可讀的方式。

使用 async/await

我們用 async/await 語法重寫上述解決方案:

async function poll(retry, interval) {
  while (retry >= 0) {
    const value = await apiCall().catch((e) => {}); 
    if (value !== null) return value;
    await sleep(interval);
    retry--;
  }

  return null;
}

我想大多數人都會覺得上面的解決方案更有可讀性,因為我們能夠使用所有正常的語言結構,如迴圈、非同步操作的 try-catch 等。

這可能是 async/await 的最大賣點--使我們能夠以同步的方式編寫非同步程式碼。另一方面,這可能是對 async/await 最常見的反對意見的來源,稍後再談這個問題。

順便說一下,await甚至有正確的運算子優先順序,所以await a + await b 等於(await a) + (await b),而不是讓我們說await (a + await b)

async/await 在同步和非同步程式碼中提供了統一的體驗

async/await的另一個好處是,await自動將任何非Promise(non-thenables)包裝成 Promises 。await的語義等同於Promise.resolve,這意味著可以 await 任何東西:

function fetchValue() {
  return 1;
}

async function fn() {
  const val = await fetchValue();
  console.log(val); // 1
}

// 上面等同於下面

function fn() {
  Promise.resolve(fetchValue()).then((val) => {
    console.log(val); // 1
  });
}

如果我們將 then 方法附加到從 fetchValue 返回的數字 1 上,就會出現以下錯誤。

function fetchValue() {
  return 1;
}

function fn() {
  fetchValue().then((val) => {
    console.log(val);
  });
}

fn(); // ❌ Uncaught TypeError: fetchValue(...).then is not a function

最後, 從 async 函式返回的任何東西都是一個 Promise:

Object.prototype.toString.call((async function () {})()); // '[object Promise]'

async/await 提供更好的錯誤堆疊跟蹤

V8工程師Mathias寫了一篇名為Asynchronous stack traces: why await beats Promise#then() 的文章,介紹了為什麼與 Promise相比,引擎更容易捕捉和儲存 async/await 的堆疊跟蹤。事例如下:

async function foo() {
  await bar();
  return 'value';
}

function bar() {
  throw new Error('BEEP BEEP');
}

foo().catch((error) => console.log(error.stack));

// Error: BEEP BEEP
//     at bar (<anonymous>:7:9)
//     at foo (<anonymous>:2:9)
//     at <anonymous>:10:1

async 版本正確地捕獲了錯誤堆疊跟蹤。

我們再來看看 Promise 版本。

function foo() {
  return bar().then(() => 'value');
}

function bar() {
  return Promise.resolve().then(() => {
    throw new Error('BEEP BEEP');
  });
}

foo().catch((error) => console.log(error.stack));

// Error: BEEP BEEP  at <anonymous>:7:11

堆疊跟蹤丟失。從匿名的箭頭函式切換到命名的函式宣告有一點幫助,但幫助不大:

function foo() {
  return bar().then(() => 'value');
}

function bar() {
  return Promise.resolve().then(function thisWillThrow() {
    throw new Error('BEEP BEEP');
  });
}

foo().catch((error) => console.log(error.stack));

// Error: BEEP BEEP
//    at thisWillThrow (<anonymous>:7:11)

async/await 常見反對意見

async/await 主要有兩種常見的反對意見。

首先,當獨立的非同步函式呼叫可以用Promise.all併發處理時,如果我們還大量使用async/await 可能會導致濫用,這樣會造成開發者不去試圖瞭解 Promise 的幕後是如何工作,而只是一味的使用 async/await

第二種情況更為微妙。一些函數語言程式設計愛好者認為 async/await 會招致指令式程式設計。從 FP 程式設計師的角度來看,能夠使用迴圈和 try catch 並不是一件好事,因為這些語言結構意味著副作用,並鼓勵使用不那麼理想的錯誤處理。

我對這種說法待保留意見。FP程式設計師理所當然地關心他們程式中的確定性。他們希望對自己的程式碼有絕對的信心。為了達到這個目的,需要一個複雜的型別系統,其中包括Result等型別。但我不認為async/await本身與FP不相容。

無論如何,對於大多數人來說,包括我在內,FP仍然是一種後天的味道(儘管我確實認為FP超級酷,而且我正在慢慢學習它)。async/await提供的正常控制流語句和try catch錯誤處理,對於我們在 JavaScript 中協調複雜的非同步操作是非常寶貴的。這正是為什麼說 "async/await只是一種語法糖" 是一種輕描淡寫的說法。

程式碼部署後可能存在的BUG沒法實時知道,事後為了解決這些BUG,花了大量的時間進行log 除錯,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug

原文:https://www.zhenghao.io/posts/await-vs-promise

交流

有夢想,有乾貨,微信搜尋 【大遷世界】 關注這個在凌晨還在刷碗的刷碗智。

本文 GitHub https://github.com/qq449245884/xiaozhi 已收錄,有一線大廠面試完整考點、資料以及我的系列文章。

相關文章