【譯】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/await
和 promise
並聲稱它是下一代 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),
};
}
複製程式碼
這段程式碼看上去沒有什麼問題,但是它是錯誤的。
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) {
// 錯誤,這會導致`序列執行`
// 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
的好處如下:
- 簡單、傳統,如果你有諸如
Java
或C++
程式語言經歷,理解起來不費事。 - 在一個
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); });
複製程式碼
這個實現有兩個瑕疵:
- 它是
promise
和async
的混合函式。你需要理解promise
才能讀懂它。 - 錯誤處理在返回之前,這不是很直觀。
結論
ES7
的 async/await
特性對 JS
非同步程式設計是個巨大的改進。它讓程式碼可讀性更好、更方便除錯。但是想要正確的使用他們,你必須徹底瞭解 promise
。因為它只是個語法糖,它依賴的技術仍然是 promise
。