[譯]帶你理解 Async/await

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

Async functions

讓咱們以 async 這個關鍵字開始。它能夠被放置在任何函數前面,像下面這樣:git

async function f() {
  return 1;
}

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

例如,如下的代碼就返回了一個以 1 爲結果的解析後的 promise, 讓咱們試一下:json

async function f() {
  return 1;
}

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

... 咱們也能夠顯式返回一個 promise,結果是同樣的:api

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

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

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

Await

語法以下:app

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

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

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

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 能夠幫助咱們同時處理多個異步任務。

原文連接

相關文章
相關標籤/搜索