原文地址: hackernoon.com/javascri

原文作者: Charlee Li翻譯作者: Xixi20160512

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)。

不僅僅是可讀性,async/await 有瀏覽器的原生支持。到今天為止,所有主流瀏覽器都支持 async 函數。

所有主流瀏覽器都支持 async 函數。(圖片來源:https://caniuse.com/)

原生支持意味著你不需要編譯代碼。更重要的是,這個將有助於調試。當你在 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 可能會產生誤導

一些文章把 async/await 和 Promise 進行了比較,同時說它是 JavaScript 非同步編程演變過程中的下一代解決方案,對此我不敢苟同。Async/await 是一個提升,但它僅僅是一個語法糖,它將不會完全的改變我們的編程風格。

實質上,async 函數仍然是 promise。你必須理解 promises 之後才能正確的使用 async 函數,更糟糕的是,大多數情況下你必須同時使用 promises 和 async 函數。

思考一下上面例子中使用到 的 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) {
// 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

最標準的(也是我推薦的)處理方式是使用 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) ,使用這種方式可以在控制台中展示所有的調用棧記錄。
  • 使用 Reject,例如, return Promise.reject(error) ,這個方式等價於 throw error ,因此不推薦使用這種方式。

使用 try...catch 的優點有以下這些:

  • 簡單,傳統。只要你有其他語言的經驗,例如 C++ 或 Java,理解這種處理方式將不會有任何困難。
  • 你可以將多個 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 函數同時返回錯誤的值和正常的值。可以從下面這個博客中了解到更詳細的的介紹:

How to write async await without try-catch blocks in Javascript

*ES7 Async/await allows us as developers to write asynchronous JS code that look synchronous. In current JS version we…*blog.grossman.io

簡而言之,你能夠像下面這樣使用 async 函數:

[err, user] = await to(UserModel.findById(1));

我個人並不喜歡這種處理方式,因為它把 Go 語言的編程風格帶到了 JavaScript 中,這樣顯得不自然,但是在某些情況下這種方式會很有用。

使用 .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);
});

這種處理方式有兩個次要的問題:

  • 這種方式混合了 promises 和 async 函數。你仍然需要理解 promises 的運行原理之後才能讀懂它。
  • 錯誤處理在正常流程之前,這樣是不太直觀的。

結論

在 ES7 中引入的 async/await 關鍵字無疑是對 JavaScript 非同步編程的一大加強。它能夠把代碼變得更易於閱讀和調試。然後,為了正確的使用它們,必須要完全理解 promises,因為它們不過是語法糖,底層的技術仍然是 promises。

希望這篇文章能夠給你一些關於 async/await 的啟發,同時能夠幫助你避免一些常見的錯誤。感謝閱讀,如果喜歡的話,請為我點贊。

推薦閱讀:

相关文章