這篇文章翻譯自 ASYNC/AWAIT WILL MAKE YOUR CODE SIMPLER,這是一篇寫於2017年八月的文章,並由某專欄提名爲17年十大必讀文章。在掘金上沒找到這篇文章的翻譯(其實沒仔細找),就想試着本身翻譯一下。翻譯的很差的地方,還望你們指出,針對我水平就好不要質疑掘金的水平(上次文章評論耿耿於懷 ̄▽ ̄),謝謝。javascript
有時,現代JavaScript項目會脫離咱們的掌控。其中一個主要的罪魁禍首就是雜亂的處理異步的任務,致使寫出了又長又複雜又深層嵌套的代碼塊。JavaScript如今提供了一個新的處理這些操做的語法,他甚至能把最錯綜複雜的操做轉化成爲簡潔並且可讀性高的代碼html
首先來進行一點科普。 在90年代末期, Ajax是異步JavaScript的第一個重大突破。 這個技術可讓網站在html加載以後獲取和展現新的數據。對於當時大部分網站的那種須要從新下載整個個頁面來展現一個部份內容的更新來講,它是革命性的創新。這項技術(在jQuery中經過捆綁成爲輔助函數而聞名)在整個21世界主導了web開發,同時ajax在今天也是網站用來檢索數據的主要技術,但xml卻被json大規模的取代java
當NodeJS在2009年第一次發佈的時候,服務端的一個主要的關注點就是容許程序優雅的處理併發。當時大部分的服務端語言使用阻塞代碼完成的這種方式來處理I/O操做,直到它結束處理I/O操做以後再繼續進行以前的代碼運行。取而代之,NodeJS利用事件循環體系,使用了一種相似ajax語法的工做方式:一旦非阻塞的異步操做完成以後,就可讓開發者分配的回調函數被觸發。node
幾年以後,一個新的叫作「promises」的標準出如今nodejs和瀏覽器環境中,他提供了一套更強大也更標準化的方式去構建異步操做。promises 仍舊使用基於回調的格式,可是爲異步操做的鏈式調用和構建提供了統一的語法。promises,這種由流行的開源庫所創造的標準,最終在2015年被加入了原生JavaScript。web
promises雖然是一個重大的改進,但仍舊會在某些狀況下產生冗長難讀的代碼。ajax
如今,咱們有了一個新的解決方案。chrome
async/await 是一種容許咱們像構建沒有回調函數的普通函數同樣構建promises的新語法(從 .net和c#借鑑而來)。 這個是一個極好的JavaScript的增長功能,在去年被加進了JavaScript ES7,它甚至能夠用來簡化幾乎全部現存的js應用。編程
咱們將會舉幾個例子。json
這些代碼例子不須要加載任何的三方庫。**Async/await 已經在在最新版本的chrome,Firefox,Safari,和edge 得到全面支持,因此你能夠在瀏覽器的控制檯中試着運行這些示例。**此外,async/await 語法能夠在Node的7.6版本及其以上運行, Babel 以及TypeScript 也一樣支持async/await 語法。Async和await 現在徹底能夠在任何JavaScript項目中使用c#
若是你想在你的電腦上跟隨咱們的腳步探尋async,咱們就將會使用這個虛擬的API Class。這個類經過返回promise對象來模擬網絡的調用的過程,而且這些promise對象將會在被調用的200ms以後使用resolve函數將簡單的數據做爲參數傳遞出去。
class Api { constructor () { this.user = { id: 1, name: 'test' } this.friends = [ this.user, this.user, this.user ] this.photo = 'not a real photo' } getUser () { return new Promise((resolve, reject) => { setTimeout(() => resolve(this.user), 200) }) } getFriends (userId) { return new Promise((resolve, reject) => { setTimeout(() => resolve(this.friends.slice()), 200) }) } getPhoto (userId) { return new Promise((resolve, reject) => { setTimeout(() => resolve(this.photo), 200) }) } throwError () { return new Promise((resolve, reject) => { setTimeout(() => reject(new Error('Intentional Error')), 200) }) } } 複製代碼
每一個例子將會按順序執行相同的三個操做:檢索一個用戶,檢索他們的朋友,以及檢索他們的照片。最後,咱們將在控制檯輸出上述的三個結果。
下面是使用嵌套的promise回調函數的實現方法
function callbackHell () { const api = new Api() let user, friends api.getUser().then(function (returnedUser) { user = returnedUser api.getFriends(user.id).then(function (returnedFriends) { friends = returnedFriends api.getPhoto(user.id).then(function (photo) { console.log('callbackHell', { user, friends, photo }) }) }) }) } 複製代碼
這可能對於任何JavaScript使用者來講再熟悉不過了。這個代碼塊有着很是簡單的目的,而且很長並且高層級嵌套,還以一大羣的括號結尾
})
})
})
}
複製代碼
在真實的代碼庫中,每一個回調函數均可能會至關長,這可能會致使產生一些很是冗長並且高層級嵌套的函數。咱們通常管這種在回調的回調中使用回調的代碼叫「回調地獄」
更糟糕的是,沒有辦法進行錯誤檢查,因此任何一個回調均可能會做爲一個未處理的Promise rejection 而引起不易察覺的地失敗。
讓咱們看看咱們是否是能改進一下
function promiseChain () { const api = new Api() let user, friends api.getUser() .then((returnedUser) => { user = returnedUser return api.getFriends(user.id) }) .then((returnedFriends) => { friends = returnedFriends return api.getPhoto(user.id) }) .then((photo) => { console.log('promiseChain', { user, friends, photo }) }) } 複製代碼
promise的一個很好的特性就是他們可以經過在每一個回調內部返回另一個promise對象而進行鏈式操做。這個方法能夠將全部的回調視做爲平級的。此外,咱們還可使用箭頭函數來縮寫回調的表達式。
這個變體明顯比以前的那個嘗試更易讀,並且還有很好的序列感。然而,很遺憾,依舊很冗長,看起來還有點複雜
有沒有可能咱們不使用任何的回調函數?不可能嗎?有想過只用7行就實現它的可能性嗎?
async function asyncAwaitIsYourNewBestFriend () { const api = new Api() const user = await api.getUser() const friends = await api.getFriends(user.id) const photo = await api.getPhoto(user.id) console.log('asyncAwaitIsYourNewBestFriend', { user, friends, photo }) } 複製代碼
變得更好了有沒有?在promise以前調用await暫停了函數流直到promise 處於resolved狀態,而後將結果賦值給等號左邊的變量。這個方式能讓咱們編寫一個就像是一個正常的同步命令同樣的異步操做流程。
我想你如今和我同樣,對這個特性感到十分的激動有沒有?!
注意「async」關鍵詞是在整個函數聲明的開始聲明的。咱們必需要這麼作,由於其實它將整個函數轉化成爲一個promise。咱們將會在稍後研究它。
Async/await讓之前的十分複雜的操做變得特別簡單,好比說, 加入咱們想按順序取回每一個用戶的朋友列表該怎麼辦?
下面是如何按照順序獲取每一個朋友列表的方式,這可能看起來很像很普通的promise。
function promiseLoops () { const api = new Api() api.getUser() .then((user) => { return api.getFriends(user.id) }) .then((returnedFriends) => { const getFriendsOfFriends = (friends) => { if (friends.length > 0) { let friend = friends.pop() return api.getFriends(friend.id) .then((moreFriends) => { console.log('promiseLoops', moreFriends) return getFriendsOfFriends(friends) }) } } return getFriendsOfFriends(returnedFriends) }) } 複製代碼
咱們建立了一個內部函數用來經過回調鏈式的promises獲取朋友的朋友,直到列表爲空。O__O 咱們的確實現了功能,很棒棒,可是咱們其實使用了一個十分複雜的方案來解決一個至關簡單的任務。
注意 - 使用
promise.all()
來嘗試簡化PromiseLoops()
函數會致使它表現爲一個有着徹底不一樣的功能的函數。這個代碼段的目的是按順序(一個接着一個)運行操做,但Promise.all
是同時運行全部異步操做(一次性運行全部)。可是,值得強調的是, Async/await 與Promise.all()
結合使用仍舊十分的強大,就像咱們下一個小節所展現的那樣。
這個可能就十分的簡單了。
async function asyncAwaitLoops () { const api = new Api() const user = await api.getUser() const friends = await api.getFriends(user.id) for (let friend of friends) { let moreFriends = await api.getFriends(friend.id) console.log('asyncAwaitLoops', moreFriends) } } 複製代碼
不須要寫任何的遞歸Promise,只有一個for循環。看到了吧,這就是你的人生益友-Async/Await
逐個獲取每一個朋友列表彷佛有點慢,爲何不採起並行執行呢?咱們可使用async/await 來實現這個需求嗎?
顯然,能夠的。你的朋友它能夠解決任何問題。:)
async function asyncAwaitLoopsParallel () { const api = new Api() const user = await api.getUser() const friends = await api.getFriends(user.id) const friendPromises = friends.map(friend => api.getFriends(friend.id)) const moreFriends = await Promise.all(friendPromises) console.log('asyncAwaitLoopsParallel', moreFriends) } 複製代碼
爲了並行的運行這些操做,要先生成成運行的promise數組,並把它做爲一個參數傳給Promise.all()。它返回給咱們一個惟一的promise對象可讓咱們進行await, 這個promise對象一旦全部的操做都完成了就將會變成resolved狀態。
然而,這篇文章到目前爲止尚未說到那個異步編程的重要問題:錯誤處理。 不少代碼庫的災難源頭就在於異步的錯誤處理一般涉及到爲每一個操做寫單獨的錯誤處理的回調。由於將錯誤放到調用堆棧的頂部會很複雜,而且一般須要在每一個回調的開始明確檢查是否有錯誤拋出。這種方法是十分繁瑣冗長並且容易出錯的。何況,在一個promise中拋出的任何異常若是沒有被正確捕獲的話,都會產生一個不被察覺的失敗,從而致使代碼庫有由於不完整錯誤檢驗而產生的「不可見錯誤」。
讓咱們從新回到以前的例子中給每一種嘗試添加錯誤處理。咱們將在獲取用戶圖片以前使用一個額外的函數api.throwError()
來檢測錯誤處理。
讓咱們來看看最糟糕的寫法:
function callbackErrorHell () { const api = new Api() let user, friends api.getUser().then(function (returnedUser) { user = returnedUser api.getFriends(user.id).then(function (returnedFriends) { friends = returnedFriends api.throwError().then(function () { console.log('Error was not thrown') api.getPhoto(user.id).then(function (photo) { console.log('callbackErrorHell', { user, friends, photo }) }, function (err) { console.error(err) }) }, function (err) { console.error(err) }) }, function (err) { console.error(err) }) }, function (err) { console.error(err) }) } 複製代碼
太噁心了。除了真的很長很醜這個缺點以外,控制流也是很是不直觀,由於他是從外層進入,而不是像正常的可讀性高的代碼同樣那種是由上至下的。太糟糕了,咱們繼續第二個嘗試。
咱們能夠經過使用一種promise-catch組合(先promise再捕獲再promise再再捕獲)的方式來改進一下。
function callbackErrorPromiseChain () { const api = new Api() let user, friends api.getUser() .then((returnedUser) => { user = returnedUser return api.getFriends(user.id) }) .then((returnedFriends) => { friends = returnedFriends return api.throwError() }) .then(() => { console.log('Error was not thrown') return api.getPhoto(user.id) }) .then((photo) => { console.log('callbackErrorPromiseChain', { user, friends, photo }) }) .catch((err) => { console.error(err) }) } 複製代碼
顯然比以前的好太多,經過利用鏈式promise的最後的那個單個的catch函數,咱們能夠爲全部的操做提供單個錯誤處理。可是,依舊有點複雜,咱們仍是必需要使用特殊的回調函數來處理異步錯誤,而不是像處理普通的JavaScript錯誤同樣處理異步錯誤。
咱們能夠作的更好。
async function aysncAwaitTryCatch () { try { const api = new Api() const user = await api.getUser() const friends = await api.getFriends(user.id) await api.throwError() console.log('Error was not thrown') const photo = await api.getPhoto(user.id) console.log('async/await', { user, friends, photo }) } catch (err) { console.error(err) } } 複製代碼
這裏,咱們將整個操做封裝在一個正常的try/catch 塊中。這樣的話,咱們就可使用一樣的方式從同步代碼和一步代碼中拋出並捕獲錯誤。顯然,簡單的多;)
我在以前提到說,任何帶上async
標籤的函數實際上返回了一個promise對象。這可讓咱們組合異步控制流變得十分的簡單。
好比說,咱們能夠從新配置以前的那些例子來返回用戶數據而不是輸出它,而後咱們能夠經過調用async函數做爲一個promise對象來檢索數據。
async function getUserInfo () { const api = new Api() const user = await api.getUser() const friends = await api.getFriends(user.id) const photo = await api.getPhoto(user.id) return { user, friends, photo } } function promiseUserInfo () { getUserInfo().then(({ user, friends, photo }) => { console.log('promiseUserInfo', { user, friends, photo }) }) } 複製代碼
更好的是,咱們也能夠在接收的函數中使用async/await語法,從而生成一個徹底清晰易懂,甚至很精煉的異步編程代碼塊。
async function awaitUserInfo () { const { user, friends, photo } = await getUserInfo() console.log('awaitUserInfo', { user, friends, photo }) } 複製代碼
若是咱們如今須要檢索前十個用戶的全部數據呢?
async function getLotsOfUserData () { const users = [] while (users.length < 10) { users.push(await getUserInfo()) } console.log('getLotsOfUserData', users) } 複製代碼
要求併發的狀況下呢?還要有嚴謹的錯誤處理呢?
async function getLotsOfUserDataFaster () { try { const userPromises = Array(10).fill(getUserInfo()) const users = await Promise.all(userPromises) console.log('getLotsOfUserDataFaster', users) } catch (err) { console.error(err) } } 複製代碼
隨着單頁JavaScript web程序的興起和對NodeJS的普遍採用,如何優雅的處理併發對於JavaScript開發人員來講比任何以往的時候都顯得更爲重要。Async/Await緩解了許多由於控制流問題而致使bug遍地的這個困擾着JavaScript代碼庫數十年的問題,而且幾乎能夠保證讓任何異步代碼塊變的更精煉,更簡單,更自信。並且近期async/await 已經在幾乎全部的主流瀏覽器以及nodejs上面得到全面支持,所以如今正是將這些技術集成到本身的代碼實踐以及項目中的最好時機。
加入到reddit的討論中