異步函數可能會一直存在,但有些人認為 async/await 可能會被拋棄。
為什麼?
一個常見的誤解是 async/await 和 promise 是完全不同的東西。
但其實 async/await 是基於 promise 的。
不要因為你使用了 promise 就被 promise 鏈給野蠻綁架了。
在本文中,我們將瞭解 async/await 如何讓開發人員的生活變得更輕鬆,以及為什麼要停止使用 promise 鏈。
讓我們來看看一個 promise 鏈的例子:
getIssue()
.then(issue => getOwner(issue.ownerId))
.then(owner => sendEmail(owner.email, 'Some text'))
現在讓我們看看 async/await 的等效代碼:
const issue = await getIssue()
const owner = await getOwner(issue.ownerId)
await sendEmail(owner.email, 'Some text')
它看起來就像簡單的語法糖,對嗎?
與大多數人一樣,我發現自己的代碼看起來很簡單、乾淨、易於閱讀。但是,在修改代碼時,似乎比預期的困難一些。
但這一點也不奇怪,這正是 promise 鏈的問題所在。下面讓我們看看這是為什麼。
易於閱讀,易於維護
假設我們需要對之前的代碼做出一個很小的修改(例如,我們需要在電子郵件內容中提及問題編號,比如“Some text #issue-number”)。
我們該怎麼做?對於 async/await 版本,改起來很簡單:
const issue = await getIssue()
const owner = await getOwner(issue.ownerId)
await sendEmail(owner.email, `Some text #${issue.number}`) // tiny change here
前兩行不受影響,第三行只需要稍微改動一點點。
那麼 promise 鏈版本呢?
在.then() 中,我們可以訪問 owner,但不能訪問 issue。看看,promise 鏈從這裡開始就變得有點混亂了。我們可以試著這樣修改:
getIssue()
.then(issue => {
return getOwner(issue.ownerId)
.then(owner => sendEmail(owner.email, `Some text #${issue.number}`))
})
正如你所看到的,一個小的調整就需要修改好幾行代碼(如 getOwner(issue.ownerId))。
代碼在不斷髮生變化
在開發新功能時尤其如此。例如,如果我們需要將異步調用 getSettings() 返回的結果包含在電子郵件內容中,該怎麼辦?
它可能看起來像這樣:
const settings = await getSettings() // we added this
const issue = await getIssue()
const owner = await getOwner(issue.ownerId)
await sendEmail(owner.email,
`Some text #${issue.number}. ${settings.emailFooter}`) // minor change here
如果使用 promise 鏈該怎樣實現?可能是這樣:
Promise.all([getIssue(), getSettings()])
.then(([issue, settings]) => {
return getOwner(issue.ownerId)
.then(owner => sendEmail(owner.email,
`Some text #${issue.number}. ${settings.emailFooter}`))
})
但是,對我來說,這些代碼顯得有點亂。每當我們需要做出修改時,都需要修改很多代碼,這實在太噁心了!
因為我不想再嵌套 then() 調用,我可以並行地調用 getIssue() 和 getSettings(),所以我使用了 Promise.all(),然後進行一些解構。確實,這個版本與 await 版本相比更好,因為它可以並行運行 ,但它仍然難以閱讀。
我們是否可以優化 await 版本,讓它可以並行運行而不需要犧牲代碼的可讀性?讓我們來看看:
const settings = getSettings() // we don't await here
const issue = await getIssue()
const owner = await getOwner(issue.ownerId)
await sendEmail(owner.email,
`Some text #${issue.number}. ${(await settings).emailFooter}`) // we do it here
我刪除了 settings 右側的 await,並在 sendEmail() 前面加上了 await。我創建了一個 promise,但在需要用到這個值之前不需要等待。與此同時,其他代碼可以並行運行。就這麼簡單!
你不需要 Promise.all() 了
我已經演示瞭如何在不使用 Promise.all() 的情況下輕鬆有效地並行運行 promise。這意味著你不再需要 Promise.all() 了,對吧。
有些人可能會爭辯說,還有一個情況,也就是當你有一個值數組時,你需要將它映射到一個 promise 數組。例如,你有一個要讀取的文件名的數組,或者你需要下載的 URL 的數組,等等。
我認為他們錯了。我的建議是使用外部庫來處理併發。例如,我會使用 bluebird 中的 Promise.map(),因為它支持設置併發限制。如果我要下載 N 個文件,可以指定同時下載的文件個數不超過 M 個。
你可以在任何地方使用 await
async/await 可以幫你簡化你要做的事情。想象一下,如果使用 promise 鏈,下面這些表達式有多複雜。但是如果使用 async/await,它們就會簡單得多。
const value = await foo() || await bar()
const value = calculateSomething(await foo(), await bar())
還說服不了你?
假設你對代碼可閱讀性和易維護性不感興趣,相反,你更喜歡複雜性,那麼好吧。
在代碼中使用 promise 鏈時,開發者每次在調用 then() 時都會創建新函數。這會佔用更多內存,而且這些函數總是處在另一個上下文中。因此,這些函數變成了閉包,這使垃圾回收變得更加困難。此外,這些匿名函數通常會汙染堆棧跟蹤。
現在,我們討論的是堆棧跟蹤:現在有一個提議(https://docs.google.com/document/d/13Sy_kBIJGP0XT34V1CV3nkWya4TwYx9L3Yv45LdGB6Q/edit)用於為異步函數實現更好的堆棧跟蹤。
只要開發人員堅持只使用異步函數和異步生成器,並且不會手動編寫 promise 代碼,因為如果使用了 promise 鏈,就無法實現更好的堆棧跟蹤。
這也是總是使用 async/await 的另一個原因!
如何遷移
首先:開始使用異步函數並停止使用 promise 鏈。
其次,你可能已經發現 Visual Studio Code 可以非常方便地幫你實現遷移(視頻地址:https://twitter.com/umaar/status/1045655069478334464)。
結論
- async/await 已得到廣泛支持,除非你需要支持 IE。
- async/await 代碼具有更好的可讀性和可維護性。
- 出於一些技術原因,最好是隻使用 async/await。
- 藉助 Visual Studio Code 或其他 IDE,你可以輕鬆地遷移現有的 promise 鏈代碼!
閱讀更多 程序猿猩球 的文章