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

翻譯練習javascript

原博客地址:JavaScript async/await: The Good Part, Pitfalls and How to Usejava

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

async/await的好處

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

// 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代碼行設置一個斷點。app

另外一個不太明顯的好處就是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

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有一些想法,也能讓你避免一些常見的錯誤。

相關文章
相關標籤/搜索