寫在文章前
這篇文章翻譯自 ASYNC/AWAIT WILL MAKE YOUR CODE SIMPLER,這是一篇寫於2017年八月的文章,並由某專欄提名為17年十大必讀文章。在掘金上沒找到這篇文章的翻譯(其實沒仔細找),就想試著自己翻譯一下。翻譯的不好的地方,還望大家指出,針對我水平就好不要質疑掘金的水平(上次文章評論耿耿於懷 ̄▽ ̄),謝謝。
[翻譯] Async/Await 使你的程式碼更簡潔
或者說,我如何學習不使用回撥函式並且愛上ES8
有時,現代JavaScript專案會脫離我們的掌控。其中一個主要的罪魁禍首就是雜亂的處理非同步的任務,導致寫出了又長又複雜又深層巢狀的程式碼塊。JavaScript現在提供了一個新的處理這些操作的語法,他甚至能把最錯綜複雜的操作轉化成為簡潔而且可讀性高的程式碼
背景
AJAX (Asynchronous JavaScript And XML)
首先來進行一點科普。 在90年代末期, Ajax是非同步JavaScript的第一個重大突破。 這個技術可以讓網站在html載入之後獲取和展示新的資料。對於當時大部分網站的那種需要重新下載整個個頁面來展示一個部分內容的更新來說,它是革命性的創新。這項技術(在jQuery中通過捆綁成為輔助函式而聞名)在整個21世界主導了web開發,同時ajax在今天也是網站用來檢索資料的主要技術,但xml卻被json大規模的取代
NodeJS
當NodeJS在2009年第一次釋出的時候,服務端的一個主要的關注點就是允許程式優雅的處理併發。當時大部分的服務端語言使用阻塞程式碼完成的這種方式來處理I/O操作,直到它結束處理I/O操作之後再繼續進行之前的程式碼執行。取而代之,NodeJS利用事件迴圈體系,使用了一種類似ajax語法的工作方式:一旦非阻塞的非同步操作完成之後,就可以讓開發者分配的回撥函式被觸發。
Promises
幾年之後,一個新的叫做“promises”的標準出現在nodejs和瀏覽器環境中,他提供了一套更強大也更標準化的方式去構建非同步操作。promises 仍舊使用基於回撥的格式,但是為非同步操作的鏈式呼叫和構建提供了統一的語法。promises,這種由流行的開源庫所創造的標準,最終在2015年被加入了原生JavaScript。
promises雖然是一個重大的改進,但仍舊會在某些情況下產生冗長難讀的程式碼。
現在,我們有了一個新的解決方案。
async/await 是一種允許我們像構建沒有回撥函式的普通函式一樣構建promises的新語法(從 .net和c#借鑑而來)。 這個是一個極好的JavaScript的增加功能,在去年被加進了JavaScript ES7,它甚至可以用來簡化幾乎所有現存的js應用。
Examples
我們將會舉幾個例子。
這些程式碼例子不需要載入任何的三方庫。**Async/await 已經在在最新版本的chrome,Firefox,Safari,和edge 獲得全面支援,所以你可以在瀏覽器的控制檯中試著執行這些示例。**此外,async/await 語法可以在Node的7.6版本及其以上執行, Babel 以及TypeScript 也同樣支援async/await 語法。Async和await 如今完全可以在任何JavaScript專案中使用
Setup
如果你想在你的電腦上跟隨我們的腳步探尋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回撥函式
下面是使用巢狀的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 而引發不易察覺的地失敗。
第二個嘗試 – 鏈式promise
讓我們看看我們是不是能改進一下
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物件而進行鏈式操作。這個方法可以將所有的回撥視作為平級的。此外,我們還可以使用箭頭函式來縮寫回撥的表示式。
這個變體明顯比之前的那個嘗試更易讀,而且還有很好的序列感。然而,很遺憾,依舊很冗長,看起來還有點複雜
第三個嘗試 Async/Await
有沒有可能我們不使用任何的回撥函式?不可能嗎?有想過只用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。我們將會在稍後研究它。
LOOPS(迴圈)
Async/await讓以前的十分複雜的操作變得特別簡單,比如說, 加入我們想按順序取回每個使用者的朋友列表該怎麼辦?
第一個嘗試 – 遞迴的promise迴圈
下面是如何按照順序獲取每個朋友列表的方式,這可能看起來很像很普通的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/Await的for迴圈
這個可能就十分的簡單了。
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
PARALLEL OPERATIONS(並行操作)
逐個獲取每個朋友列表似乎有點慢,為什麼不採取並行執行呢?我們可以使用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狀態。
Error handling (錯誤處理)
然而,這篇文章到目前為止還沒有說到那個非同步程式設計的重要問題:錯誤處理。 很多程式碼庫的災難源頭就在於非同步的錯誤處理通常涉及到為每個操作寫單獨的錯誤處理的回撥。因為將錯誤放到呼叫堆疊的頂部會很複雜,並且通常需要在每個回撥的開始明確檢查是否有錯誤丟擲。這種方法是十分繁瑣冗長而且容易出錯的。況且,在一個promise中丟擲的任何異常如果沒有被正確捕獲的話,都會產生一個不被察覺的失敗,從而導致程式碼庫有因為不完整錯誤檢驗而產生的“不可見錯誤”。
讓我們重新回到之前的例子中給每一種嘗試新增錯誤處理。我們將在獲取使用者圖片之前使用一個額外的函式api.throwError()
來檢測錯誤處理。
第一個嘗試 – promise的錯誤回撥函式
讓我們來看看最糟糕的寫法:
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捕獲方法
我們可以通過使用一種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錯誤一樣處理非同步錯誤。
第三個嘗試-正常的try/catch塊
我們可以做的更好。
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 塊中。這樣的話,我們就可以使用同樣的方式從同步程式碼和一步程式碼中丟擲並捕獲錯誤。顯然,簡單的多;
)
Composition(組合)
我在之前提到說,任何帶上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)
}
}複製程式碼
Conclusion(結論)
隨著單頁JavaScript web程式的興起和對NodeJS的廣泛採用,如何優雅的處理併發對於JavaScript開發人員來說比任何以往的時候都顯得更為重要。Async/Await緩解了許多因為控制流問題而導致bug遍地的這個困擾著JavaScript程式碼庫數十年的問題,並且幾乎可以保證讓任何非同步程式碼塊變的更精煉,更簡單,更自信。而且近期async/await 已經在幾乎所有的主流瀏覽器以及nodejs上面獲得全面支援,因此現在正是將這些技術整合到自己的程式碼實踐以及專案中的最好時機。
討論時間
加入到reddit的討論中
來源:https://juejin.im/post/5a6c1db1518825733a30efbf?utm_medium=fe&utm_source=weixinqun