1. 程式人生 > 實用技巧 >JavaScript async/await:優點、陷阱及如何使用

JavaScript async/await:優點、陷阱及如何使用

翻譯練習

原部落格地址:JavaScript async/await: The Good Part, Pitfalls and How to Use

ES7中引進的async/await是對JavaScript的非同步程式設計的一個極大改進。它提供了一種同步程式碼編寫風格來獲取非同步資源的選項,卻不阻塞主程序。然後,想很好地使用它也很棘手。在這篇文章中我們將通過不同的角度去探討async/await,然後展示如何正確、有效地使用它們。

async/await的好處

async/await帶給我們最大的好處就是同步的編碼風格。讓我們看看下面的例子。

// async/await
async getBooksByAuthorWithAwait(authorId) {
  const books = await bookModel.fetchAll();
  return books.filter(b => b.authorId === authorId);
}
// promise
getBooksByAuthorWithPromise(authorId) {
  return bookModel.fetchAll()
    .then(books => books.filter(b => b.authorId === authorId));
}

顯而易見,async/await的版本比promise的版本更容易理解。如果你忽略await關鍵詞,程式碼看起來就像其他的的同步語言,比如說Python

最棒的地方不僅僅是可讀性。async/await擁有瀏覽器的原生支援。目前為止,所有的主流瀏覽器都完全支援async函式。

原生支援意味著你不用轉譯程式碼。更重要的是,它便於除錯。當你在函式的入口處設定一個斷點,然後步入await這行程式碼,你將看到偵錯程式在bookModel.fetchAll()執行的時候停了一會,然後移動到下一步.filter程式碼行。這比promise的情況簡單多了,在promise的情況下,你還需要.filter程式碼行設定一個斷點。

另一個不太明顯的好處就是async關鍵詞。它宣告getBooksByAuthorWithAwait()函式返回的值保證是一個promise,所以呼叫者可以安全的呼叫getBooksByAuthorWithAwait().then(...) 或者 await getBooksByAuthorWithAwait()。想一下下面這種情況(不好的實踐):

getBooksByAuthorWithPromise(authorId) {
  if (!authorId) {
    return null;
  }
  return bookModel.fetchAll()
    .then(books => books.filter(b => b.authorId === authorId));
}

在上面的程式碼中,getBooksByAuthorWithPromise可能返回一個promise(正常情況)或者返回null(例外情況),這種情況下呼叫者沒辦法安全地呼叫.then()。使用async宣告,這種情況變得不可能。

Async/await可能會誤導

一些文章對async/awaitPromise進行比較,然後宣稱async/awaitJavaScript非同步程式設計進化的下一代,恕我不敢苟同。Async/await是一種改進而不僅僅是一種語法糖,它並不能完全改變我們的程式設計風格。

本質上,async函式仍然還是promise。在你正確的使用async函式之前你需要先理解promise,更有甚者,很多時候你需要同時使用async函式的promise

考慮一下上面例子中的getBooksByAuthorWithAwait()getBooksByAuthorWithPromises()函式。請注意它們不僅僅功能完全相同,介面也完全相同。

這意味著,如果你直接呼叫getBooksByAuthorWithAwait(),它將返回一個promise

嗯,這並不是一件壞事。只用await的名字給人一種,好的,這個可以把非同步函式轉換成同步函式,但這種想法完全是錯誤的。

Async/await的陷阱

那麼使用async/await的時候可能會煩那些錯誤呢?這裡有一些常見的錯誤。

太連續

儘管await可以使你的程式碼看起來像同步程式碼,時刻謹記,它們仍然是非同步程式碼,必須小心的去避免太過於連續。

async getBooksAndAuthor(authorId) {
  const books = await bookModel.fetchAll();
  const author = await authorModel.fetch(authorId);
  return {
    author,
    books: books.filter(book => book.authorId === authorId),
  };
}

這段程式碼看起來邏輯上是正確的,但事實上它是錯誤的。

  1. await bookModel.fetchAll() 會一直等到fetchAll() 返回。
  2. 然後await authorModel.fetch(authorId)被呼叫。

請注意,authorModel.fetch(authorId)並不依賴bookModel.fetchAll()的結果,事實上它們可以並行呼叫。然而,這裡使用await讓這兩個函式順序執行,結果它的執行總時間比並行執行的要更長。

下面是正確的方式:

async getBooksAndAuthor(authorId) {
  const bookPromise = bookModel.fetchAll();
  const authorPromise = authorModel.fetch(authorId);
  const book = await bookPromise;
  const author = await authorPromise;
  return {
    author,
    books: books.filter(book => book.authorId === authorId),
  };
}

或者更有甚者,你想一個接一個的獲取一個列表,你必須依靠promises

async getAuthors(authorIds) {
  // WRONG, this will cause sequential calls
  // const authors = _.map(
  //   authorIds,
  //   id => await authorModel.fetch(id));
  // CORRECT
  const promises = _.map(authorIds, id => authorModel.fetch(id));
  const authors = await Promise.all(promises);
}

簡而言之,你需要非同步地去思考工作量,然後使用await同步地編寫程式碼。在負責的工作流中直接使用promise可能會更簡單。

錯誤處理

使用promise時,一個非同步的函式有兩種可能返回的結果:resolved 值和rejected值。我們可以使用.then去處理正常情況,.catch去處理異常情況。然而,使用async/await時,異常處理可能比較難弄。

try…catch

最標準的(也是最推薦的)方式是使用try...catch語句。當await一個呼叫時,任何rejeced值都會被當做一個異常丟擲。下面是一個例子:

class BookModel {
  fetchAll() {
    return new Promise((resolve, reject) => {
      window.setTimeout(() => { reject({'error': 400}) }, 1000);
    });
  }
}
// async/await
async getBooksByAuthorWithAwait(authorId) {
try {
  const books = await bookModel.fetchAll();
} catch (error) {
  console.log(error);    // { "error": 400 }
}

被捕獲到的錯誤正是被rejected的值。在我們捕獲到異常以後,我們有以下幾個處理方式:

  • 處理異常然後返回一個正常值。(在catch程式碼塊中不返回任何值相當一返回undefined,這也算一個正常值。)
  • 如果你想讓呼叫者處理它,那就把它丟擲去。你也可以像throw error這樣直接丟擲整個error物件,這允許你在promise鏈中去使用async getBooksByAuthorWithAwait()函式(例如:你仍然可以像這樣去呼叫getBooksByAuthorWithAwait().then(...).catch(error => ...));或者你可以使用Error物件對錯誤進行包裝,像這樣:throw new Error(error),這樣,當錯誤在控制條打印出來的時候,這給你提供一個完整的錯誤堆疊跟蹤。
  • Reject它,像return Promise.reject(error)這樣。這根throw error一樣,所以不推薦這樣做。

使用try...catch的好處如下:

  • 簡單,傳統。只要你有其他的語言的經驗,如Java 或者 C++,沒有任何困難去理解它。
  • 如果沒有必要去處理每個步驟的錯誤的話,你可以在一個try...catch中去包裹多個await呼叫,這樣可以在一個地方去處理錯誤。

這種做法也有一個缺陷。因為try...catch會捕獲所有的異常,一些不經常被promise捕獲的錯誤也會被捕獲。想一下下面這個例子:

class BookModel {
  fetchAll() {
    cb();    // note `cb` is undefined and will result an exception
    return fetch('/books');
  }
}
try {
  bookModel.fetchAll();
} catch(error) {
  console.log(error);  // This will print "cb is not defined"
}

執行這段程式碼,你在控制檯會得到一個黑色字型的錯誤資訊:ReferenceError: cb is not defined。這個錯誤是consol.log輸出的,而不是JavaScript輸出的。有時候這會是致命的:如果BookModel被一系列的函式呼叫深深包裹著,而其中的一個呼叫吞掉了這個錯誤,那麼找到一個像這樣的未定義錯誤將會十分困難。

使函式返回兩個值

還有一種錯誤處理方式是受到了Go語言的啟發。它允許非同步函式同時返回錯誤和結果。

簡而言之,你可以這樣使用非同步函式:

[err, user] = await to(UserModel.findById(1));
使用catch

我們將要介紹的最後一種方法是繼續使用catch

回想一下await的功能:它會等待promise完成它的工作。同時也回想一下promise.catch()也會返回一個promise。所以我們可以這樣去寫錯誤處理:

// books === undefined if error happens,
// since nothing returned in the catch statement
let books = await bookModel.fetchAll()
  .catch((error) => { console.log(error); })

這種做法有兩個小問題:

  • promise和非同步函式混合使用。為了讀懂它,你仍需要了解promise是如何工作的。
  • 錯誤處理先於正常的操作,這是不直觀的。

結論

ES7引進的async/await關鍵詞對JavaScript的非同步程式設計來說絕對是一個進步。它使得程式碼的閱讀和除錯都更簡單。然而,為了正確的使用它,你必須去完全的理解promise,因為它不在僅僅是語法糖,它的底層原理仍然是promise

希望這篇文章能讓你對async/await有一些想法,也能讓你避免一些常見的錯誤。