1. 程式人生 > 實用技巧 >【譯】async/await 優點、陷阱以及如何使用 (經驗總結)

【譯】async/await 優點、陷阱以及如何使用 (經驗總結)

https://juejin.im/post/5b2075375188257d4044c783

ES7 推出的 async/await 特性對 JS 的非同步程式設計是一個重大的改進。在不阻塞主執行緒的情況下,它為我們提供了使用同步程式碼風格去非同步獲取資源的能力。當然使用它也是需要一些技巧,這篇文章我們從不同角度去探索 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 已經被瀏覽器原生支援。如今,所有主流瀏覽器已經完全支援

原生支援,意味著你不必轉換程式碼,而更重要的是有利於除錯。當你在函式的 await 程式碼行打上斷點,然後步進到下一行時,你會發現偵錯程式在 bookModel.fetchAll() 操作的時候進行了短暫的停留,然後才真正的步進到 .filter 程式碼行!這比 promise 除錯更方便,因為你需要在 .fliter 程式碼行再打一個斷點。

另一個很少被人注意到的優點是 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 並聲稱它是下一代 JS 非同步程式設計,而我不同意這種觀點。async/await 的確是一種改進,但它不過是個語法糖,不會徹底改變我們的程式設計風格。

本質來說,async 函式仍然是 promises。在正確的使用 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) {
  // 錯誤,這會導致`序列執行`
  // const authors = _.map(
  //   authorIds,
  //   id => await authorModel.fetch(id));

  // 正確
  const promises = _.map(authorIds, id => authorModel.fetch(id));
  const authors = await Promise.all(promises);
}
複製程式碼

簡而言之,你仍然需要把工作流當成是非同步的,然後嘗試使用 await 去寫同步程式碼。在更加複雜的工作流中,直接使用 promise 可能更方便。

錯誤處理

結合 promises,一個非同步函式只有兩個可能的返回值:resolve值reject值,然後我們可以使用 .then() 處理正常情況、.catch() 處理異常情況。但是 async/await 的錯誤處理就需要點技巧了。

try...catch

最常見(也是我推薦)的方法就是使用 try..catch。當 await 一個操作時,操作中任何 reject值 都會當作異常丟擲。見程式碼:

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 }
}
複製程式碼

輸出的錯誤物件正是 reject值。捕獲異常之後,我們可以使用如下方法處理它們:

  • 處理異常,返回一個正常值(在 catch 程式碼塊不使用 return 語句等同於 return undefined;,當然這也算是個正常值)。
  • 如果你想讓呼叫者處理異常,那就丟擲。你可以直接丟擲異常物件,如 throw error,這樣允許你在 async getBooksByAuthorWithAwait() 函式上使用 promise 鏈式操作(即:getBooksByAuthorWithAwait().then(...).catch(error => ...));或者使用 Error 物件包裝你的錯誤物件,如 throw new Error(error),這樣在控制檯檢視錯誤時,你可以看到完整的堆疊記錄。
  • reject錯誤物件,如 return Promise.reject(error)。這等同於第一種做法,所以不推薦。

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

  • 簡單、傳統,如果你有諸如 JavaC++ 程式語言經歷,理解起來不費事。
  • 在一個 try...catch 程式碼塊中你可以在 try 程式碼塊包裹多行 await 語句,並且如果前置錯誤處理沒有必要的話,你可以在一個地方(即 catch 程式碼塊)處理錯誤。

這個方案仍然有它的瑕疵,try...catch 可以捕獲程式碼塊內的所有錯誤,包括那些不被 promises 捕獲的錯誤。見程式碼:

class BookModel {
  fetchAll() {
    cb();    // `cb` 因為沒有被定義所有會導致異常
    return fetch('/books');
  }
}
try {
  bookModel.fetchAll();
} catch(error) {
  console.log(error);  // 這裡列印 "cb is not defined"
}
複製程式碼

執行這段程式碼,你會在控制檯得到 ReferenceError: cb is not defined 黑色字型輸出資訊。你要知道,這裡的錯誤是通過 console.log() 輸出的,並不是 JS 本身丟擲(JS 丟擲錯誤是紅色字型)。有時這會很致命:如果 BookModel 被其它一些函式呼叫深深巢狀、包裹,其中一個呼叫吞併異常,那麼想找到例子中的這種錯誤就會變得極其困難。

讓函式返回所有值

Go 語言啟發,另一種處理錯誤的方法就是允許 async 函式返回異常結果兩個值(請參閱 How to write async await without try-catch blocks in Javascript),即你可以這樣使用 async 函式:

[err, user] = await to(UserModel.findById(1));
複製程式碼

我個人不建議使用這種實現,因為它把 Go 語言的風格帶到了 JS,這讓我感覺很不自然,但是個別情況下,使用它是極其合適的。

使用.catch()

最後一個方法就是繼續使用 .catch()

回想一下 await 的作用:它等待 promise 完成工作,也請記住 promise.catch() 也會返回一個 promise!所以我們可以這些處理錯誤:

// 如果發生異常,但是 catch 語句沒有顯示返回,那麼 books === undefined
let books = await bookModel.fetchAll()
  .catch((error) => { console.log(error); });
複製程式碼

這個實現有兩個瑕疵:

  • 它是 promiseasync 的混合函式。你需要理解 promise 才能讀懂它。
  • 錯誤處理在返回之前,這不是很直觀。

結論

ES7async/await 特性對 JS 非同步程式設計是個巨大的改進。它讓程式碼可讀性更好、更方便除錯。但是想要正確的使用他們,你必須徹底瞭解 promise。因為它只是個語法糖,它依賴的技術仍然是 promise