原文地址: https://hackernoon.com/javascript-async-await-the-good-part-pitfalls-and-how-to-use-9b759ca21cda原文作者: Charlee Li翻譯作者: Xixi20160512
原文地址: https://hackernoon.com/javascript-async-await-the-good-part-pitfalls-and-how-to-use-9b759ca21cda
async/await 是在 ES7 版本中引入的,它對於 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)。
await
不僅僅是可讀性,async/await 有瀏覽器的原生支持。到今天為止,所有主流瀏覽器都支持 async 函數。
async
所有主流瀏覽器都支持 async 函數。(圖片來源:https://caniuse.com/)
原生支持意味著你不需要編譯代碼。更重要的是,這個將有助於調試。當你在 async 方法的入口打一個斷點並且步進到 await 這一行的時候,你將會看到調試器在 bookModel.fetchAll() 這個函數執行的時候等待了一會兒,然後才會走到接下來的 .filter 這一行!和 promise 的示例比較起來,這個容易多了,因為你必須在 .filter 這一行再打一個斷點。
bookModel.fetchAll()
.filter
調試 async 函數。調試器會在 await 這一行等待執行完成然後才會移動到下一行。
另一個不那麼明顯的好處就是 async 關鍵字。它聲明了 getBooksByAuthorWithAwait() 方法返回的是一個 promise,因此調用者可以像 getBooksByAuthorWithAwait().then(...) 或者 await getBooksByAuthorWithAwait() 這樣安全的調用。看一下這個例子(不好的實踐):
getBooksByAuthorWithAwait()
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 進行聲明的時候,這個問題就不會存在了。
getBooksByAuthorWithPromise
null
.then()
一些文章把 async/await 和 Promise 進行了比較,同時說它是 JavaScript 非同步編程演變過程中的下一代解決方案,對此我不敢苟同。Async/await 是一個提升,但它僅僅是一個語法糖,它將不會完全的改變我們的編程風格。
實質上,async 函數仍然是 promise。你必須理解 promises 之後才能正確的使用 async 函數,更糟糕的是,大多數情況下你必須同時使用 promises 和 async 函數。
思考一下上面例子中使用到 的 getBooksByAuthorWithAwait() 和 getBooksByAuthorWithPromises() 。請注意,它們不僅是有相同的功能,同時也有相同的介面。
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 這兩次調用就變成了串列的了,花費的總時間將會遠超並行的方式。
authorModel.fetch(authorId)
以下是正確的使用方式:
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 來說,異常處理可能會有點詭異。
.catch()
最標準的(也是我推薦的)處理方式是使用 try...catch 表達式。當 await 一個函數調用的時候,任何 rejected 的值都會以異常的形式拋出來。這裡有個例子:
try...catch
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
throw error;
async getBooksByAuthorWithAwait()
getBooksByAuthorWithAwait().then(...).catch(error => ...)
Error
throw new Error(error)
return Promise.reject(error)
throw error
使用 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 被一系列函數調用深深地封閉起來了,同時,其中某一個調用將這個錯誤處理掉了,這時候就很難像這樣去發現這個錯誤了。
ReferenceError: cb is not defined
console.log()
BookModel
另外一個錯誤處理的方式是由 Go 語言啟發的。它允許 async 函數同時返回錯誤的值和正常的值。可以從下面這個博客中了解到更詳細的的介紹:
How to write async await without try-catch blocks in Javascript
簡而言之,你能夠像下面這樣使用 async 函數:
[err, user] = await to(UserModel.findById(1));
我個人並不喜歡這種處理方式,因為它把 Go 語言的編程風格帶到了 JavaScript 中,這樣顯得不自然,但是在某些情況下這種方式會很有用。
我要介紹的最後一種處理方式是仍然使用 .catch()。
回憶一下 await 的功能:它會等待一個 promise 完成它的任務。同時請回憶一下, promise.catch() 也會返回一個 promise!因此我們可以像下面這樣處理錯誤處理的方式:
promise.catch()
// 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 的啟發,同時能夠幫助你避免一些常見的錯誤。感謝閱讀,如果喜歡的話,請為我點贊。
推薦閱讀: