Javascript async/await

2023-02-17 10:53 更新

async/await 是以更舒適的方式使用 promise 的一種特殊語法,同時它也非常易于理解和使用。

async function

讓我們以 ?async? 這個關(guān)鍵字開始。它可以被放置在一個函數(shù)前面,如下所示:

async function f() {
  return 1;
}

在函數(shù)前面的 “async” 這個單詞表達了一個簡單的事情:即這個函數(shù)總是返回一個 promise。其他值將自動被包裝在一個 resolved 的 promise 中。

例如,下面這個函數(shù)返回一個結(jié)果為 1 的 resolved promise,讓我們測試一下:

async function f() {
  return 1;
}

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

……我們也可以顯式地返回一個 promise,結(jié)果是一樣的:

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

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

所以說,async 確保了函數(shù)返回一個 promise,也會將非 promise 的值包裝進去。很簡單,對吧?但不僅僅這些。還有另外一個叫 await 的關(guān)鍵詞,它只在 async 函數(shù)內(nèi)工作,也非??帷?

await

語法如下:

// 只在 async 函數(shù)內(nèi)工作
let value = await promise;

關(guān)鍵字 await 讓 JavaScript 引擎等待直到 promise 完成(settle)并返回結(jié)果。

這里的例子就是一個 1 秒后 resolve 的 promise:

async function f() {

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

  let result = await promise; // 等待,直到 promise resolve (*)

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

f();

這個函數(shù)在執(zhí)行的時候,“暫?!痹诹?nbsp;(*) 那一行,并在 promise settle 時,拿到 result 作為結(jié)果繼續(xù)往下執(zhí)行。所以上面這段代碼在一秒后顯示 “done!”。

讓我們強調(diào)一下:await 實際上會暫停函數(shù)的執(zhí)行,直到 promise 狀態(tài)變?yōu)?settled,然后以 promise 的結(jié)果繼續(xù)執(zhí)行。這個行為不會耗費任何 CPU 資源,因為 JavaScript 引擎可以同時處理其他任務(wù):執(zhí)行其他腳本,處理事件等。

相比于 promise.then,它只是獲取 promise 的結(jié)果的一個更優(yōu)雅的語法。并且也更易于讀寫。

不能在普通函數(shù)中使用 ?await?

如果我們嘗試在非 async 函數(shù)中使用 await,則會報語法錯誤:

function f() {
  let promise = Promise.resolve(1);
  let result = await promise; // Syntax error
}

如果我們忘記在函數(shù)前面寫 async 關(guān)鍵字,我們可能會得到一個這個錯誤。就像前面說的,await 只在 async 函數(shù)中有效。

讓我們拿 Promise 鏈 那一章的 showAvatar() 例子,并將其改寫成 async/await 的形式:

  1. 我們需要用 ?await? 替換掉 ?.then? 的調(diào)用。
  2. 另外,我們需要在函數(shù)前面加上 ?async? 關(guān)鍵字,以使它們能工作。
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();

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

現(xiàn)代瀏覽器在 modules 里允許頂層的 ?await?

在現(xiàn)代瀏覽器中,當(dāng)我們處于一個 module 中時,那么在頂層使用 await 也是被允許的。我們將在 模塊 (Module) 簡介 中詳細(xì)學(xué)習(xí) modules。

例如:

// 我們假設(shè)此代碼在 module 中的頂層運行
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();

console.log(user);

如果我們沒有使用 modules,或者必須兼容 舊版本瀏覽器 ,那么這兒還有一個通用的方法:包裝到匿名的異步函數(shù)中。

像這樣:

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

?await? 接受 “thenables”

像 promise.then 那樣,await 允許我們使用 thenable 對象(那些具有可調(diào)用的 then 方法的對象)。這里的想法是,第三方對象可能不是一個 promise,但卻是 promise 兼容的:如果這些對象支持 .then,那么就可以對它們使用 await

這有一個用于演示的 Thenable 類,下面的 await 接受了該類的實例:

class Thenable {
  constructor(num) {
    this.num = num;
  }
  then(resolve, reject) {
    alert(resolve);
    // 1000ms 后使用 this.num*2 進行 resolve
    setTimeout(() => resolve(this.num * 2), 1000); // (*)
  }
}

async function f() {
  // 等待 1 秒,之后 result 變?yōu)?2
  let result = await new Thenable(1);
  alert(result);
}

f();

如果 await 接收了一個非 promise 的但是提供了 .then 方法的對象,它就會調(diào)用這個 .then 方法,并將內(nèi)建的函數(shù) resolve 和 reject 作為參數(shù)傳入(就像它對待一個常規(guī)的 Promise executor 時一樣)。然后 await 等待直到這兩個函數(shù)中的某個被調(diào)用(在上面這個例子中發(fā)生在 (*) 行),然后使用得到的結(jié)果繼續(xù)執(zhí)行后續(xù)任務(wù)。

Class 中的 async 方法

要聲明一個 class 中的 async 方法,只需在對應(yīng)方法前面加上 async 即可:

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

new Waiter()
  .wait()
  .then(alert); // 1(alert 等同于 result => alert(result))

這里的含義是一樣的:它確保了方法的返回值是一個 promise 并且可以在方法中使用 await。

Error 處理

如果一個 promise 正常 resolve,await promise 返回的就是其結(jié)果。但是如果 promise 被 reject,它將 throw 這個 error,就像在這一行有一個 throw 語句那樣。

這個代碼:

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

……和下面是一樣的:

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

在真實開發(fā)中,promise 可能需要一點時間后才 reject。在這種情況下,在 await 拋出(throw)一個 error 之前會有一個延時。

我們可以用 try..catch 來捕獲上面提到的那個 error,與常規(guī)的 throw 使用的是一樣的方式:

async function f() {

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

f();

如果有 error 發(fā)生,執(zhí)行控制權(quán)馬上就會被移交至 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,那么由異步函數(shù) f() 的調(diào)用生成的 promise 將變?yōu)?rejected。我們可以在函數(shù)調(diào)用后面添加 .catch 來處理這個 error:

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

// f() 變成了一個 rejected 的 promise
f().catch(alert); // TypeError: failed to fetch // (*)

如果我們忘了在這添加 .catch,那么我們就會得到一個未處理的 promise error(可以在控制臺中查看)。我們可以使用在 使用 promise 進行錯誤處理 一章中所講的全局事件處理程序 unhandledrejection 來捕獲這類 error。

?async/await? 和 ?promise.then/catch?

當(dāng)我們使用 async/await 時,幾乎就不會用到 .then 了,因為 await 為我們處理了等待。并且我們使用常規(guī)的 try..catch 而不是 .catch。這通常(但不總是)更加方便。

但是當(dāng)我們在代碼的頂層時,也就是在所有 async 函數(shù)之外,我們在語法上就不能使用 await 了,所以這時候通常的做法是添加 .then/catch 來處理最終的結(jié)果(result)或掉出來的(falling-through)error,例如像上面那個例子中的 (*) 行那樣。

?async/await? 可以和 ?Promise.all? 一起使用

當(dāng)我們需要同時等待多個 promise 時,我們可以用 Promise.all 把它們包裝起來,然后使用 await

// 等待結(jié)果數(shù)組
let results = await Promise.all([
  fetch(url1),
  fetch(url2),
  ...
]);

如果出現(xiàn) error,也會正常傳遞,從失敗了的 promise 傳到 Promise.all,然后變成我們能通過使用 try..catch 在調(diào)用周圍捕獲到的異常(exception)。

總結(jié)

函數(shù)前面的關(guān)鍵字 ?async? 有兩個作用:

  1. 讓這個函數(shù)總是返回一個 promise。
  2. 允許在該函數(shù)內(nèi)使用 ?await?。

Promise 前的關(guān)鍵字 await 使 JavaScript 引擎等待該 promise settle,然后:

  1. 如果有 error,就會拋出異常 —— 就像那里調(diào)用了 ?throw error? 一樣。
  2. 否則,就返回結(jié)果。

這兩個關(guān)鍵字一起提供了一個很好的用來編寫異步代碼的框架,這種代碼易于閱讀也易于編寫。

有了 async/await 之后,我們就幾乎不需要使用 promise.then/catch,但是不要忘了它們是基于 promise 的,因為有些時候(例如在最外層作用域)我們不得不使用這些方法。并且,當(dāng)我們需要同時等待需要任務(wù)時,Promise.all 是很好用的。

任務(wù)


用 async/await 來重寫

重寫下面這個來自 Promise 鏈 一章的示例代碼,使用 async/await 而不是 .then/catch

function loadJson(url) {
  return fetch(url)
    .then(response => {
      if (response.status == 200) {
        return response.json();
      } else {
        throw new Error(response.status);
      }
    });
}

loadJson('https://javascript.info/no-such-user.json')
  .catch(alert); // Error: 404

解決方案

解析在代碼下面:

async function loadJson(url) { // (1)
  let response = await fetch(url); // (2)

  if (response.status == 200) {
    let json = await response.json(); // (3)
    return json;
  }

  throw new Error(response.status);
}

loadJson('https://javascript.info/no-such-user.json')
  .catch(alert); // Error: 404 (4)

解析:

  1. 將函數(shù) ?loadJson? 變?yōu)?nbsp;?async?。
  2. 將函數(shù)中所有的 ?.then? 都替換為 ?await?。
  3. 我們可以返回 ?return response.json()? 而不用等待它,像這樣:
  4. if (response.status == 200) {
      return response.json(); // (3)
    }

    然后外部的代碼就必須 await 這個 promise resolve。在本例中它無關(guān)緊要。

  5. ?loadJson? 拋出的 error 被 ?.catch? 處理了。在這兒我們我們不能使用 ?await loadJson(…)?,因為我們不是在一個 ?async? 函數(shù)中。

使用 async/await 重寫 "rethrow"

下面你可以看到 “rethrow” 的例子。讓我們來用 async/await 重寫它,而不是使用 .then/catch。

同時,我們可以在 demoGithubUser 中使用循環(huán)以擺脫遞歸:在 async/await 的幫助下很容易實現(xiàn)。

class HttpError extends Error {
  constructor(response) {
    super(`${response.status} for ${response.url}`);
    this.name = 'HttpError';
    this.response = response;
  }
}

function loadJson(url) {
  return fetch(url)
    .then(response => {
      if (response.status == 200) {
        return response.json();
      } else {
        throw new HttpError(response);
      }
    });
}

// 詢問用戶名,直到 github 返回一個合法的用戶
function demoGithubUser() {
  let name = prompt("Enter a name?", "iliakan");

  return loadJson(`https://api.github.com/users/${name}`)
    .then(user => {
      alert(`Full name: ${user.name}.`);
      return user;
    })
    .catch(err => {
      if (err instanceof HttpError && err.response.status == 404) {
        alert("No such user, please reenter.");
        return demoGithubUser();
      } else {
        throw err;
      }
    });
}

demoGithubUser();

解決方案

這里沒有什么技巧。只需要將 demoGithubUser 中的 .catch 替換為 try...catch,然后在需要的地方加上 async/await 即可:

class HttpError extends Error {
  constructor(response) {
    super(`${response.status} for ${response.url}`);
    this.name = 'HttpError';
    this.response = response;
  }
}

async function loadJson(url) {
  let response = await fetch(url);
  if (response.status == 200) {
    return response.json();
  } else {
    throw new HttpError(response);
  }
}

// 詢問用戶名,直到 github 返回一個合法的用戶
async function demoGithubUser() {

  let user;
  while(true) {
    let name = prompt("Enter a name?", "iliakan");

    try {
      user = await loadJson(`https://api.github.com/users/${name}`);
      break; // 沒有 error,退出循環(huán)
    } catch(err) {
      if (err instanceof HttpError && err.response.status == 404) {
        // 循環(huán)將在 alert 后繼續(xù)
        alert("No such user, please reenter.");
      } else {
        // 未知的 error,再次拋出(rethrow)
        throw err;
      }
    }
  }


  alert(`Full name: ${user.name}.`);
  return user;
}

demoGithubUser();

在非 async 函數(shù)中調(diào)用 async 函數(shù)

我們有一個名為 f 的“普通”函數(shù)。你會怎樣調(diào)用 async 函數(shù) wait() 并在 f 中使用其結(jié)果?

async function wait() {
  await new Promise(resolve => setTimeout(resolve, 1000));

  return 10;
}

function f() {
  // ……這里你應(yīng)該怎么寫?
  // 我們需要調(diào)用 async wait() 并等待以拿到結(jié)果 10
  // 記住,我們不能使用 "await"
}

P.S. 這個任務(wù)其實很簡單,但是對于 async/await 新手開發(fā)者來說,這個問題卻很常見。


解決方案

在這種情況下,知道其內(nèi)部工作原理會很有幫助。

只需要把 async 調(diào)用當(dāng)作 promise 對待,并在它的后面加上 .then 即可:

async function wait() {
  await new Promise(resolve => setTimeout(resolve, 1000));

  return 10;
}

function f() {
  // 1 秒后顯示 10
  wait().then(result => alert(result));
}

f();


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號