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/await
和Promise
進行比較,然後宣稱async/await
是JavaScript
非同步程式設計進化的下一代,恕我不敢苟同。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),
};
}
這段程式碼看起來邏輯上是正確的,但事實上它是錯誤的。
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
同步地編寫程式碼。在負責的工作流中直接使用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
有一些想法,也能讓你避免一些常見的錯誤。