【譯】async/await 優勢、陷阱以及如何使用

最近公事甚多,很久沒學習了,本身也擼了個小網站,歡迎 star。javascript

JavaScript async/await: The Good Part, Pitfalls and How to Usejava

ES7 推出的 async/await 特性對 JS 的異步編程是一個重大的改進。在不阻塞主線程的狀況下,它爲咱們提供了使用同步代碼風格去異步獲取資源的能力。固然使用它也是須要一些技巧,這篇文章咱們從不一樣角度去探索 async/await,爲你展現如何正確、高效的使用它們。編程

async/await 優勢

它最大的優勢就是給咱們帶來同步代碼風格。見代碼:json

// 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 關鍵字,那麼代碼就如同其餘同步編程語言,如 Pythonpromise

優勢不只僅是可讀性,async/await 已經被瀏覽器原生支持。現在,全部主流瀏覽器已經徹底支持瀏覽器

原生支持,意味着你沒必要轉換代碼,而更重要的是有利於調試。當你在函數的 await 代碼行打上斷點,而後步進到下一行時,你會發現調試器在 bookModel.fetchAll() 操做的時候進行了短暫的停留,而後才真正的步進到 .filter 代碼行!這比 promise 調試更方便,由於你須要在 .fliter 代碼行再打一個斷點。安全

另外一個不多被人注意到的優勢是 async 關鍵字。它代表了 getBooksByAuthorWithAwait() 函數的返回值必定是個 promise,因此它的調用者可使用 getBooksByAuthorWithAwait().then(...) 或者安全的使用 await getBooksByAuthorWithAwait()。見代碼(錯誤的實踐!):babel

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

本質來講,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

相關文章
相關標籤/搜索