譯文出自:閃電礦工翻譯組javascript
原文地址: JavaScript async/awaitjava
原文做者: Charlee Ligit
倉庫原文連接:issuegithub
譯者: Xixi20160512typescript
async/await
是在 ES7 版本中引入的,它對於 JavaScript 中的異步編程而言是一個巨大的提高。它可讓咱們以同步的方式處理異步的流程,同時不會阻塞主線程。可是,想要用好這一特性,可能須要動點腦筋。本文中,咱們將從不一樣的角度探討 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
函數。安全
全部主流瀏覽器都支持 async
函數。(圖片來源:caniuse.com/)app
原生支持意味着你不須要編譯代碼。更重要的是,這個將有助於調試。當你在 async 方法的入口打一個斷點而且步進到 await
這一行的時候,你將會看到調試器在 bookModel.fetchAll()
這個函數執行的時候等待了一下子,而後纔會走到接下來的 .filter
這一行!和 promise 的示例比較起來,這個容易多了,由於你必須在 .filter
這一行再打一個斷點。
調試 async 函數。調試器會在 await 這一行等待執行完成而後纔會移動到下一行。
另外一個不那麼明顯的好處就是 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
(特殊狀況下),返回 null 的時候調用者不能安全的調用 .then()
。使用 async
進行聲明的時候,這個問題就不會存在了。
一些文章把 async/await 和 Promise 進行了比較,同時說它是 JavaScript 異步編程演變過程當中的下一代解決方案,對此我不敢苟同。Async/await 是一個提高,但它僅僅是一個語法糖,它將不會徹底的改變咱們的編程風格。
實質上,async 函數仍然是 promise。你必須理解 promises 以後才能正確的使用 async 函數,更糟糕的是,大多數狀況下你必須同時使用 promises 和 async 函數。
思考一下上面例子中使用到 的 getBooksByAuthorWithAwait()
和 getBooksByAuthorWithPromises()
。請注意,它們不只是有相同的功能,同時也有相同的接口。
這意味着若是你直接 getBooksByAuthorWithAwait()
的話,將會返回一個 promise。
固然,這並非一件很差的事情。只有 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),
};
}
複製代碼
這段代碼看起來邏輯上沒有問題。然而是不正確的。
await bookModel.fetchAll()
將會等待 fetchAll()
執行完。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
以同步的方式去編寫代碼。在複雜的流程下面,直接使用 promises 可能會更簡單。
使用 promises 的狀況下,一個異步函數會返回兩種可能的值:resolved 和 rejected。咱們可使用 .then()
來處理正常的狀況 .catch()
處理異常狀況。然而對於 async/await
來講,異常處理可能會有點詭異。
最標準的(也是我推薦的)處理方式是使用 try...catch
表達式。當 await
一個函數調用的時候,任何 rejected 的值都會以異常的形式拋出來。這裏有個例子:
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
塊中使用任何 return
表達式等價於使用 return undefined
;同時,返回的還是一個 resolved 的值。)throw error;
,這種方式容許你以 promise 鏈式的方式使用 async getBooksByAuthorWithAwait()
方法(列如,你仍然能夠像 getBooksByAuthorWithAwait().then(...).catch(error => ...)
這樣調用它);或者,你可使用 Error
對象包裝錯誤對象,例如, throw new Error(error)
,使用這種方式能夠在控制檯中展現全部的調用棧記錄。return Promise.reject(error)
,這個方式等價於 throw error
,所以不推薦使用這種方式。使用 try...catch
的優勢有如下這些:
await
調用包裝在一個 try...catch
塊中來集中處理全部錯誤,若是每一步的錯誤處理非必要的話。這種處理方式有一個缺陷。因爲 try...catch
將會捕獲這個代碼塊中的全部異常,一些其餘一般不會被 promises 捕獲的異常也會被捕獲住。考慮一下這個例子:
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
,這些文字是黑色的。這個錯誤是 console.log()
打印出來的而不是 JavaScript 自身。某些時候這將會是致命的:若是 BookModel
被一系列函數調用深深地封閉起來了,同時,其中某一個調用將這個錯誤處理掉了,這時候就很難像這樣去發現這個錯誤了。
另一個錯誤處理的方式是由 Go 語言啓發的。它容許 async 函數同時返回錯誤的值和正常的值。能夠從下面這個博客中瞭解到更詳細的的介紹:
簡而言之,你可以像下面這樣使用 async 函數:
[err, user] = await to(UserModel.findById(1));
複製代碼
我我的並不喜歡這種處理方式,由於它把 Go 語言的編程風格帶到了 JavaScript 中,這樣顯得不天然,可是在某些狀況下這種方式會頗有用。
我要介紹的最後一種處理方式是仍然使用 .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);
});
複製代碼
這種處理方式有兩個次要的問題:
在 ES7 中引入的 async/await
關鍵字無疑是對 JavaScript 異步編程的一大增強。它可以把代碼變得更易於閱讀和調試。而後,爲了正確的使用它們,必需要徹底理解 promises,由於它們不過是語法糖,底層的技術仍然是 promises。
但願這篇文章可以給你一些關於 async/await
的啓發,同時可以幫助你避免一些常見的錯誤。感謝閱讀,若是喜歡的話,請爲我點贊。