[譯]帶你理解 Async/await

savokiss發表於2019-06-16

「async/await」是 promises 的另一種更便捷更流行的寫法,同時它也更易於理解和使用。

Async functions

讓我們以 async 這個關鍵字開始。它可以被放置在任何函式前面,像下面這樣:

async function f() {
  return 1;
}

在函式前面的「async」這個單詞表達了一個簡單的事情:即這個函式總是返回一個 promise。即使這個函式在語法上返回了一個非 promise 的值,加了「async」這個關鍵字就會指示 JavaScript 引擎自動將返回值包裝成一個解析後的 promise。

例如,以下的程式碼就返回了一個以 1 為結果的解析後的 promise, 讓我們試一下:

async function f() {
  return 1;
}

f().then(alert); // 1

... 我們也可以顯式返回一個 promise,結果是一樣的:

async function f() {
  return Promise.resolve(1);
}

f().then(alert); // 1

所以說,async 確保了函式的返回值是一個 promise,也會包裝非 promise 的值。很簡單是吧?但是還沒完。還有一個關鍵字叫 await,它只在 async 函式中有效,也非常酷。

Await

語法如下:

// 只在 async 函式中有效
let value = await promise;

關鍵字 await 讓 JavaScript 引擎等待直到 promise 完成並返回結果。

這裡的例子就是一個 1 秒後解析的 promise:

async function f() {

  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("done!"), 1000)
  });

  let result = await promise; // 等待直到 promise 解析 (*)

  alert(result); // "done!"
}

f();

這個函式在執行的時候,「暫停」在了 (*) 那一行,並且當 promise 完成後,拿到 result 作為結果繼續往下執行。所以「done!」是在一秒後顯示的。

劃重點:await 字面的意思就是讓 JavaScript 引擎等待直到 promise 狀態完成,然後以完成的結果繼續執行。這個行為不會耗費 CPU 資源,因為引擎可以同時處理其他任務:執行其他指令碼,處理事件等。

相比 promise.then 來獲取 promise 結果,這只是一個更優雅的語法,同時也更易書寫。


不能在普通函式中使用 await
如果我們嘗試在非 async 函式中使用 await 的話,就會報語法錯誤:

function f() {
  let promise = Promise.resolve(1);
  let result = await promise; // 語法錯誤
}

如果函式前面沒有 async 關鍵字,我們就會得到一個語法錯誤。就像前面說的,await 只在 async 函式 中有效。


讓我們拿 Promises 鏈那一章的 showAvatar() 例子改寫成 async/await 的形式:

  1. await 替換掉 .then 的呼叫
  2. 在函式前面加上 async 關鍵字
async function showAvatar() {

  // 讀取 JSON
  let response = await fetch('/article/promise-chaining/user.json');
  let user = await response.json();

  // 讀取 github 使用者資訊
  let githubResponse = await fetch(`https://api.github.com/users/${user.name}`);
  let githubUser = await githubResponse.json();

  // 顯示頭像
  let img = document.createElement('img');
  img.src = githubUser.avatar_url;
  img.className = "promise-avatar-example";
  document.body.append(img);

  // 等待 3 秒
  await new Promise((resolve, reject) => setTimeout(resolve, 3000));

  img.remove();

  return githubUser;
}

showAvatar();

簡潔明瞭,是吧?比之前可強多了。


await 不能在頂層程式碼執行
剛開始使用 await 的人常常會忘記 await 不能用在頂層程式碼中。如,下面這樣就不行:


// 用在頂層程式碼中會報語法錯誤
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();

我們可以將其包裹在一個匿名 async 函式中,如:

(async () => {
  let response = await fetch('/article/promise-chaining/user.json');
  let user = await response.json();
  ...
})();


await 可以接收「thenables」
promise.then 那樣,await 被允許接收 thenable 物件(具有 then 方法的物件)。有些物件雖然不是 promise,但是卻相容 promise,如果這些物件支援 .then,那麼就可以對它們使用 await

下面是一個 Thenable 類,await 接收了該類的例項:

class Thenable {
  constructor(num) {
    this.num = num;
  }
  then(resolve, reject) {
    alert(resolve);
    // 1 秒後解析為 this.num*2
    setTimeout(() => resolve(this.num * 2), 1000); // (*)
  }
};

async function f() {
  // 等待 1 秒, result 變為 2
  let result = await new Thenable(1);
  alert(result);
}

f();

如果 await 接收了一個非 promise 的但是提供了 .then 方法的物件,它就會呼叫這個 then 方法,並將原生函式 resolvereject 作為引數傳入。然後 await 等到這兩個方法中的某個被呼叫(在例子中發生在(*)的那一行),再處理得到的結果。



Async methods
如果想定義一個 async 的類方法,在方法前面新增 async 就可以了:

class Waiter {
  async wait() {
    return await Promise.resolve(1);
  }
}

new Waiter()
  .wait()
  .then(alert); // 1

異常處理

如果一個 promise 正常解析,await promise 返回的就是其結果。但是如果 promise 被拒絕,就會丟擲一個錯誤,就像在那一行有個 throw 語句那樣。

這裡的程式碼:

async function f() {
  await Promise.reject(new Error("Whoops!"));
}

...和下面是一樣的:

async function f() {
  throw new Error("Whoops!");
}

在真實的環境下,promise 被拒絕前通常會等待一段時間。所以 await 會等待,然後丟擲一個錯誤。

我們可以用 try...catch 來捕獲上面的錯誤,就像對一般的 throw 語句那樣:

async function f() {

  try {
    let response = await fetch('http://no-such-url');
  } catch(err) {
    alert(err); // TypeError: failed to fetch
  }
}

f();

如果有錯誤發生,程式碼就會跳到 catch 塊中。當然也可以用 try 包裹多行 await 程式碼:

async function f() {

  try {
    let response = await fetch('/no-user-here');
    let user = await response.json();
  } catch(err) {
    // 捕獲到 fetch 和 response.json 中的錯誤
    alert(err);
  }
}

f();

如果我們不使用 try...catch,由f() 產生的 promise 就會被拒絕。我們可以在函式呼叫後新增 .catch 來處理錯誤:

async function f() {
  let response = await fetch('http://no-such-url');
}

// f() 變為一個被拒絕的 promise
f().catch(alert); // TypeError: failed to fetch // (*)

如果我們忘了新增 .catch,我們就會得到一個未處理的 promise 錯誤(顯示在控制檯)。我們可以通過在錯誤處理與 Promise 章節講的全域性事件處理器來捕獲這些。


async/awaitpromise.then/catch
當我們使用 async/await 時,幾乎就不會用到 .then 了,因為為我們await 處理了非同步等待。並且我們可以用 try...catch 來替代 .catch。這通常更加方便(當然不是絕對的)。

但是當我們在頂層程式碼,外面並沒有任何 async 函式,我們在語法上就不能使用 await 了,所以這時候就可以用 .then/catch 來處理結果和異常。

就像上面程式碼的 (*) 那行一樣。



async/await 可以和 Promise.all 一起使用
當我們需要同時等待多個 promise 時,我們可以用 Promise.all 來包裹他們,然後使用 await

// 等待多個 promise 結果
let results = await Promise.all([
  fetch(url1),
  fetch(url2),
  ...
 ]);

如果發生錯誤,也會正常傳遞:先從失敗的 promise 傳到 Promise.all,然後變成我們能用 try...catch 處理的異常。


Microtask queue

我們在微任務和事件迴圈章節講過,promise 回撥是非同步執行的。每個 .then/catch/finally 回撥首先被放入「微任務佇列」然後在當前程式碼執行完成後被執行。

Async/await 是基於 promise 的,所以它內部使用相同的微任務佇列,並且相對巨集任務來說具有更高的優先順序。

例如,看程式碼:

  • setTimeout(handler, 0),應該以零延遲執行 handler 函式。
  • let x = await f(),函式 f() 是非同步的,但是會立即執行。

那麼如果 awaitsetTimeout 下面,哪一個先執行呢?


async function f() {
  return 1;
}

(async () => {
    setTimeout(() => alert('timeout'), 0);

    await f();

    alert('await');
})();

這裡很確定:await 總是先完成,因為(作為微任務)它相比 setTimeout 具有更高的優先順序。

總結

函式前面的關鍵字 async 有兩個作用:

  1. 讓這個函式返回一個 promise
  2. 允許在函式內部使用 await

這個 await 關鍵字又讓 JavaScript 引擎等待直到 promise 完成,然後:

  1. 如果有錯誤,就會丟擲異常,就像那裡有一個 throw error 語句一樣。
  2. 否則,就返回結果,並賦值。

這兩個關鍵字一起用就提供了一個很棒的方式來控制非同步程式碼,並且易於讀寫。

有了 async/await 我們就幾乎不需要使用 promise.then/catch,但是不要忘了它們是基於 promise 的,所以在有些時候(如在最外層程式碼)我們就可以用 promise 的形式。再有就是 Promise.all 可以幫助我們同時處理多個非同步任務。

原文連結

相關文章