JavaScript 的 Async/Await 完勝 Promise 的六個(gè)理由
提醒一下各位,Node 現(xiàn)在從版本 7.6 開始就支持 async/await 了。如果你還沒有試過它,這里有一堆帶有示例的理由來說明為什么你應(yīng)該馬上采用它,并且再也不會回頭。
貌似嵌入 gist 上的代碼在 medium 原生 app 中不行,但是在移動瀏覽器上可以。如果你是在 app 中讀本文,請點(diǎn)擊共享圖標(biāo),選擇“在瀏覽器中打開”,才看得到代碼片段。
Async/await 101
對于那些從未聽說過這個(gè)話題的人來說,如下是一個(gè)簡單的介紹:
- Async/await 是一種編寫異步代碼的新方法。之前異步代碼的方案是回調(diào)和 promise。
- Async/await 實(shí)際上是建立在 promise 的基礎(chǔ)上。它不能與普通回調(diào)或者 node 回調(diào)一起用。
- Async/await 像 promise 一樣,也是非阻塞的。
- Async/await 讓異步代碼看起來、表現(xiàn)起來更像同步代碼。這正是其威力所在。
語法
假設(shè)函數(shù) getJSON 返回一個(gè)promise,而該promise的完成值是一些JSON對象。我們只想調(diào)用它,并輸出該JSON,然后返回"done"。
如下是用 promise 實(shí)現(xiàn)的代碼:
- const makeRequest = () =>
- getJSON()
- .then(data => {
- console.log(data)
- return "done"
- })
- makeRequest()
而這就是用async/await看起來的樣子:
- const makeRequest = async () => {
- console.log(await getJSON())
- return "done"
- }
- makeRequest()
這里有一些區(qū)別:
1.函數(shù)前面有一個(gè)關(guān)鍵字 async。await 關(guān)鍵字只用在用 async 定義的函數(shù)內(nèi)。所有 async函數(shù)都會隱式返回一個(gè) promise,而 promise 的完成值將是函數(shù)的返回值(本例中是 "done")。
2.上面一點(diǎn)暗示我們不能在代碼的頂層用 await,因?yàn)檫@樣就不是在 async 函數(shù)內(nèi)。
- // 這段代碼在頂層不能執(zhí)行
- // await makeRequest()
- // 這段代碼可以執(zhí)行
- makeRequest().then((result) => {
- // do something
- })
3.await getJSON() 意味著 console.log 調(diào)用會一直等待,直到 getJSON() promise 完成并打印出它的值。
為什么 Async/await 更好?
1. 簡潔干凈
看看我們少寫了多少代碼!即使在上面那個(gè)人為的示例中,很顯然我們也是節(jié)省了不少代碼。我們不必寫 .then,創(chuàng)建一個(gè)匿名函數(shù)來處理響應(yīng),或者給不需要用的變量一個(gè)名稱 data。我們還避免了代碼嵌套。這些小小的優(yōu)勢會快速累積起來,在后面的代碼中會變得更明顯。
2. 錯(cuò)誤處理
Async/await 會最終讓我們用同樣的結(jié)構(gòu)( try/catch)處理同步和異步代碼變成可能。在下面使用 promise 的示例中,如果 JSON.parse 失敗的話,try/catch 就不會處理,因?yàn)樗前l(fā)生在一個(gè) prmoise 中。我們需要在 promise 上調(diào)用 .catch,并且重復(fù)錯(cuò)誤處理代碼。這種錯(cuò)誤處理代碼會比可用于生產(chǎn)的代碼中的 console.log 更復(fù)雜。
- const makeRequest = () => {
- try {
- getJSON()
- .then(result => {
- // this parse may fail
- const data = JSON.parse(result)
- console.log(data)
- })
- // uncomment this block to handle asynchronous errors
- // .catch((err) => {
- // console.log(err)
- // })
- } catch (err) {
- console.log(err)
- }
- }
現(xiàn)在看看用 async/await 實(shí)現(xiàn)的代碼?,F(xiàn)在 catch 塊會處理解析錯(cuò)誤。
- const makeRequest = async () => {
- try { // 這個(gè)解析會失敗
- const data = JSON.parse(await getJSON()) console.log(data)
- }
- catch (err) {
- console.log(err)
- }
- }
3. 條件句
假設(shè)想做像下面的代碼一樣的事情,獲取一些數(shù)據(jù),并決定是否應(yīng)該返回該數(shù)據(jù),或者根據(jù)數(shù)據(jù)中的某些值獲取更多的細(xì)節(jié)。
- const makeRequest = () => {
- return getJSON()
- .then(data => {
- if (data.needsAnotherRequest) {
- return makeAnotherRequest(data)
- .then(moreData => {
- console.log(moreData)
- return moreData
- })
- } else {
- console.log(data)
- return data
- }
- })
- }
這些代碼看著就讓人頭疼。它只需將最終結(jié)果傳播到主 promise,卻很容易讓我們迷失在嵌套( 6 層)、大括號和返回語句中。
把這個(gè)示例用async / await 重寫,就變得更易于閱讀。
- onst makeRequest = async () => {
- const data = await getJSON()
- if (data.needsAnotherRequest) {
- const moreData = await makeAnotherRequest(data);
- console.log(moreData)
- return moreData
- } else {
- console.log(data)
- return data
- }
- }
4. 中間值
你可能發(fā)現(xiàn)自己處于一種狀態(tài),即調(diào)用你 promise1,然后用它的返回值來調(diào)用promise2,然后使用這兩個(gè) promise 的結(jié)果來調(diào)用 promise3。你的代碼很可能看起來像這樣:
- const makeRequest = () => {
- return promise1()
- .then(value1 => {
- // do something
- return promise2(value1)
- .then(value2 => {
- // do something
- return promise3(value1, value2)
- })
- })
- }
如果 promise3 不需要 value1,那么很容易就可以把 promise 嵌套變扁平一點(diǎn)。如果你是那種無法忍受的人,那么可能就會像下面這樣,在一個(gè) Promise.all中包含值 1 和 2,并避免更深層次的嵌套:
- onst makeRequest = () => {
- return promise1()
- .then(value1 => {
- // do something
- return Promise.all([value1, promise2(value1)])
- })
- .then(([value1, value2]) => {
- // do something
- return promise3(value1, value2)
- })
- }
這種方法為了可讀性而犧牲了語義。除了為了避免 promise 嵌套,沒有理由將 value1和value2并入一個(gè)數(shù)組。
不過用 async/await 的話,同樣的邏輯就變得超級簡單直觀了。這會讓你對你拼命讓 promise 看起來不那么可怕的時(shí)候所做過的所有事情感到懷疑。
- const makeRequest = async () => {
- const value1 = await promise1()
- const value2 = await promise2(value1)
- return promise3(value1, value2)
- }
5. 錯(cuò)誤棧
假如有一段鏈?zhǔn)秸{(diào)用多個(gè) promise 的代碼,在鏈的某個(gè)地方拋出一個(gè)錯(cuò)誤。
- const makeRequest = () => {
- return callAPromise()
- .then(() => callAPromise())
- .then(() => callAPromise())
- .then(() => callAPromise())
- .then(() => callAPromise())
- .then(() => {
- throw new Error("oops");
- })
- }
- makeRequest()
- .catch(err => {
- console.log(err);
- // output
- // Error: oops at callAPromise.then.then.then.then.then (index.js:8:13)
- })
從 promise 鏈返回的錯(cuò)誤棧沒有發(fā)現(xiàn)錯(cuò)誤發(fā)生在哪里的線索。更糟糕的是,這是誤導(dǎo)的;它包含的唯一的函數(shù)名是callAPromise,它完全與此錯(cuò)誤無關(guān)(不過文件和行號仍然有用)。
但是,來自async / await的錯(cuò)誤棧會指向包含錯(cuò)誤的函數(shù):
- const makeRequest = async () => {
- await callAPromise()
- await callAPromise()
- await callAPromise()
- await callAPromise()
- await callAPromise()
- throw new Error("oops");
- }
- makeRequest()
- .catch(err => {
- console.log(err);
- // output
- // Error: oops at makeRequest (index.js:7:9)
- })
當(dāng)在本地環(huán)境中開發(fā)并在編輯器中打開文件時(shí),這不是啥大事,但是當(dāng)想搞清楚來自生產(chǎn)服務(wù)器的錯(cuò)誤日志時(shí),就相當(dāng)有用了。在這種情況下,知道錯(cuò)誤發(fā)生在makeRequest中比知道錯(cuò)誤來自一個(gè)又一個(gè)的 then 要好。
6. 調(diào)試
***但是同樣重要的是,在使用 async/await 時(shí),一個(gè)殺手級優(yōu)勢是調(diào)試更容易。調(diào)試 promise 一直是如此痛苦,有兩個(gè)原因:
1.沒法在返回表達(dá)式(無函數(shù)體)的箭頭函數(shù)中設(shè)置斷點(diǎn)。
試著在此處設(shè)置斷點(diǎn)
2.如果在.then塊中設(shè)置斷點(diǎn),并使用像單步調(diào)試這類調(diào)試快捷方式,調(diào)試器不會移動到后面的 .then ,因?yàn)樗粏尾秸{(diào)試同步代碼。
有了 async/await,我們就不再需要那么多箭頭函數(shù),您可以像正常的同步調(diào)用一樣單步調(diào)試 await 調(diào)用。
總結(jié)
Async/await 是過去幾年中添加到 JavaScript 中的***革命性的功能之一。它讓我們意識到 promise 的語法有多混亂,并提供了直觀的替代。