【譯】async/await 應知應會

譯文出自:閃電礦工翻譯組javascript

原文地址: JavaScript async/awaitjava

原文做者: Charlee Ligit

倉庫原文連接:issuegithub

譯者: Xixi20160512typescript

async/await 是在 ES7 版本中引入的,它對於 JavaScript 中的異步編程而言是一個巨大的提高。它可讓咱們以同步的方式處理異步的流程,同時不會阻塞主線程。可是,想要用好這一特性,可能須要動點腦筋。本文中,咱們將從不一樣的角度探討 async/await,同時會展現如何正確和高效的使用它們。編程

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 函數。安全

68747470733a2f2f63646e2d696d616765732d312e6d656469756d2e636f6d2f6d61782f3830302f312a633662596168414255447047674d704a56616b3441672e706e67

全部主流瀏覽器都支持 async 函數。(圖片來源:caniuse.com/)app

原生支持意味着你不須要編譯代碼。更重要的是,這個將有助於調試。當你在 async 方法的入口打一個斷點而且步進到 await 這一行的時候,你將會看到調試器在 bookModel.fetchAll() 這個函數執行的時候等待了一下子,而後纔會走到接下來的 .filter 這一行!和 promise 的示例比較起來,這個容易多了,由於你必須在 .filter 這一行再打一個斷點。

2

調試 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 的啓發,同時可以幫助你避免一些常見的錯誤。感謝閱讀,若是喜歡的話,請爲我點贊。

相關文章
相關標籤/搜索