代碼詳解:Async/Await優於基礎Promises的7大原因

2020-12-04 讀芯術

全文共4898字,預計學習時長10分鐘

圖片來源:https://unsplash.com/@grohsfabian

Async/await已被引入NodeJS 7.6,當前能在幾乎所有先進的瀏覽器上運行。這絕對是自2017年以來JavaScript最好的附加語法,沒有之一。

Async/Await 101

Async/await是一種編寫異步代碼的新方法。以前編寫異步代碼會用callbacks和promises。

Async/await 實際上只是一種基於promises的糖衣語法,不能與基礎callbacks或節點callbacks一同使用。

Async/await和promises一樣,都是非堵塞式的。

Async/await讓異步代碼更具同步代碼風格,這也是其優勢所在。

句法

假設函數getJSON返回一個promise,該promise通過某個JSON對象進行解析。我們只想調用它並記錄該JSON,然後返回「done」。

以下是運用promises執行上述步驟的方式:

const makeRequest = () =>

getJSON()

.then(data => {

console.log(data)

return "done"

})

makeRequest()

以下則是用async/await的效果:

const makeRequest = async () => {

console.log(await getJSON())

return "done"

}

makeRequest()

兩者有一些不同:

1. 函數前有關鍵詞async。關鍵詞await 只能用於async定義的函數之內。任一async函數都能隱式返回promise,其解析值是從該函數返回的任意值(在此指字符串「done」)。

2. 上一點意味著不能在代碼頂部使用await,因為它不在async定義的函數範圍之中。

// this will not work in top level

// await makeRequest()

// this will work

makeRequest().then((result) => {

// do something

})

import pandas as pd

3. await getJSON() 意味著 console.log 調用會等到getJSON() promise進行解析後才輸出解析值。

為何async/await更優越?

圖片來源:https://unsplash.com/@amartino20

1. 簡潔乾淨

看看使用async/await能省去寫多少行代碼的麻煩!即使在上面這個人為設計的例子中,也能明顯看到省去了相當多行代碼。並且也無需創建一個匿名函數來處理響應,或為不需要使用的變量提供名稱數據,還能避免嵌套代碼。這些小優勢快速疊加起來,在下面的代碼示例中變得更加明顯。

2. 錯誤處理

async/ wait能用相同的結構和好用的經典 try/catch處理同步和異步錯誤。在下面帶有promises的示例中,如果JSON 解析失敗,try/catch不會進行處理,因為它要在promise中運行。因此需要在promise中調用catch並複製錯誤處理代碼,對生產就緒代碼而言,這將(希望)比console.log更精密。

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)

}

}

現在來看看使用async/await的相同代碼。catch塊此時就可以處理解析錯誤。

const makeRequest = async () => {

try {

// this parse may fail

const data = JSON.parse(await getJSON())

console.log(data)

} catch (err) {

console.log(err)

}

}

3. 限定條件

想像一下,下面的代碼能獲取一些數據,並根據數據中的一些值決定是返回數據,還是繼續獲取更多細節。

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

}

})

}

光是看著這些代碼就非常頭疼了,很容易就會迷失在各個嵌套(6級)、大括號,以及只用於將最終結果傳遞到主promise的返回語句中。

但當使用async/ await重寫這個示例時,可讀性便大大提高。

const 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. 中間值

假如調用promise e1,然後使用它返回的結果調用promise e2,接著再用這兩個promises的結果調用promise e3。得出的代碼很可能是這樣的:

const makeRequest = () => {

return promise1()

.then(value1 => {

// do something

return promise2(value1)

.then(value2 => {

// do something

return promise3(value1, value2)

})

})

}

如果promise e3不需要值1,就會輕易縮減promise嵌套。如果不能容忍這種情況發生,可以把值1和2都包含在一個promise中,避免更深的嵌套,就像這樣:

const makeRequest = () => {

return promise1()

.then(value1 => {

// do something

return Promise.all([value1, promise2(value1)])

})

.then(([value1, value2]) => {

// do something

return promise3(value1, value2)

})

}

這種方法為了保證代碼的可讀性,犧牲了語義的表達。值1和2除非是為了避免嵌套promise,沒有理由屬於同一個數組。

但使用async/ await,同樣的邏輯就會變得異常簡單和直觀。想想你花了多少時間試圖讓promises更加簡明,而你本可以用這些時間幹更多事情!

const makeRequest = async () => {

const value1 = await promise1()

const value2 = await promise2(value1)

return promise3(value1, value2)

}

5. 錯誤堆棧

假設一段代碼在一個鏈中調用多個promises,然後在鏈的某個地方出現了錯誤。

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鏈返回的錯誤堆棧並未顯示錯誤發生的位置。更糟糕的是,它具有誤導性。它包含的唯一函數名是壓根沒有造成錯誤的callAPromise (但文件和行號仍然有用)。

但async/ await中的錯誤堆棧能指出包含錯誤的函數

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)

})

如果在本地環境中進行開發並在編輯器中打開文件,這並不是一個很大的加分項。但它在理解來自生產伺服器的錯誤日誌方面卻非常有用。在這種情況下,知道錯誤發生在makeRequest中,總好過知道錯誤來自下一處的下一處的下一處……

6. 調試

使用async/ await的一個極大優勢是它更容易調試。調試promises一直是一件痛苦的事情,原因有二:

(1)不能在返回表達式(沒有主幹)的箭頭函數中設置斷點。

試試在任意一處設置斷點

(2)如果在.then塊中設置斷點並使用諸如step-over之類的調試快捷鍵,調試器不會移動到下面的.then塊,因為它只能「跨過」同步代碼。

但使用async/ await則無需過多箭頭函數,並且能像正常的同步調用一樣直接跨過await調用。

7. 可以await一切

最後,await可用於同步和異步表達式。例如,await5就相當於Promise.resolve(5)。這個乍一看來似乎沒什麼用,但在編寫一個庫或一個實用函數時,如果不知道輸入是同步還是異步,這就是一個很大的優勢了。

如果希望記錄在應用程式中執行一些API調用所花費的時間,並為此創建一個泛型函數,以下是使用promises的效果:

所有API調用都會返回promises,但如果使用相同的函數來記錄同步函數所花費的時間,會發生什麼?通常情況下會出錯,因為同步函數沒有返回一個promise。為了避免這種情況,通常會在Promise.resolve()中寫入makeRequest()。

但如果使用async/await,就不必擔心這些情況,因為await能夠安全處理任何值,不管是否有promises。

const recordTime = async (makeRequest) => {

const timeStart = Date.now();

await makeRequest(); // works for any sync or async function

const timeEnd = Date.now();

console.log('time take:', timeEnd - timeStart);

}

總結

async/await是過去幾年JavaScript上最具革新性的添加特性。它凸顯出promises語法上的混亂,並提供了簡明易懂的替代。

誠然,有人質疑使用async/await會讓異步代碼看起來不那麼明顯。但每當看見一個調用或.then塊時,就可辨認出異步代碼。人們也許需要幾個星期來適應這種新信號,但C#多年前就具有這個特性了,熟悉的它的人自然明白,這微小、暫時的不便不過是瑕不掩瑜。

留言 點讚 關注

我們一起分享AI學習與發展的乾貨

編譯組:楊敏迎、段昌蓉

相關連結:

https://dev.to/gafi/7-reasons-to-always-use-async-await-over-plain-promises-tutorial-4ej9

如需轉載,請後臺留言,遵守轉載規範

相關焦點

  • [完結篇] - 理解異步之美 --- promise與async await(三)
    因為在用法上promise要比async await難一些,而且promise本身又不是一個語法糖。沒有掌握的時候用起來就會有很多顧慮,async await卻沒有這種顧慮,用法簡單、語義清晰。下面就要開始學習async await了:不講講迭代器模式總覺得怪怪的對於java語言來說,迭代器是一個很基本的模式,list與set結構都內置了迭代器。
  • c ,net core常見異步問題關於Task,async, wait, async void
    把過程中遇到的問題分享一下:01await Task.Run()是沒有意義的,只是換了一個線程等待開始時為了將同步代碼轉換成異步代碼,簡單的就將代碼用Task.Run包起來。Task.Run會使代碼在後臺執行,但無法提高代碼的可擴展性,實際上它會消耗更多資源,因為它需要從線程池中取新的線程來執行代碼。
  • 理解異步之美--- Promise與async await(一)
    難以掌控的觸發狀態,讓你自己寫的代碼當時還可以讀懂,但是過幾天半個月之後如果不重新盤一邊邏輯,你哪知道哪個內容會先執行借用這麼一個例子首先 執行listern()下一期的內容是針對於網上常見的Promise的自我實現進行一個分析,總之一句話抓住Promise的承諾思想,就可以很好的去編寫promise的代碼。async 與await將會在下期或者下下期進行講解。
  • 10個 Javascript 小技巧幫你提升代碼質量
    獨立出來的函數有助於代碼復用。獨立出來的函數更容易被覆寫。獨立出來的函數如果擁有一個良好的命名,它本身就起到了注釋的作用。語義化將多段分離的邏輯放在不同的函數中實現,可以使代碼邏輯清晰,清楚的看到每一步在做什麼。
  • 理解異步之美——Promise與async await(二)
    承上啟下經歷了上一篇基礎的Promise講解後我覺得大家對於promise的基本用法和想法就有一定了解了。分析代碼對代碼的分析在內容的注釋上大家不要遺漏哈!!!第一段構造函數與狀態設定自己實現與官方Promise執行的對比。
  • python入門基礎之lambda匿名函數詳解
    python入門基礎之lambda匿名函數詳解剛開始學習python的時候很多人可能對於lambda函數不了解,感覺和def很混亂,下面我來介紹一下lambda函數我從一下幾個方面來介紹lambda:1、lambda函數主要用來寫一些小體量的一次性函數,避免汙染環境,同時也能簡化代碼。2、lambda起到了一種函數速寫的作用,允許在使用的代碼內嵌入一個函數的定義。他們完全是可選的(你總是能夠使用def來替代它們),但是你僅需要嵌入小段可執行代碼的情況下它們會帶來一個更簡潔的代碼結構。
  • 《輻射4》注射器步槍效果及武器彈藥代碼詳解
    那麼下面就和小編一起看看《輻射4》中注射器步槍的武器彈藥代碼和效果吧。   注射器步槍詳解   注射器步槍算是輻射4裡面數一數二獨特的武器 不僅能有多種效果 在戰術上的運用也很靈活   雖然用高火力碾壓敵人是蠻爽快的 但是像是這種Debuff型的武器也有另一種樂趣   先簡單列一下這把武器的優缺點   優點: 1.基礎命中率不低 改滿時命中為99   2.有著需多傳奇詞綴的功能(狂怒、流血、逃跑等等)   3.敵人越多=戰力越高(試想你遇到自爆兵) 靠狂怒和蒼蠅可以喝杯咖啡再悠閒的去撿裝
  • 代碼詳解——如何計算航向誤差?
    PS:我這個人代碼存檔的習慣不太好,幾年前的代碼雖然有存檔,但是沒什麼注釋,自己看都需要重新讀,所以代碼詳解欄目的代碼都是現碼(搞這個公眾號的初衷也有整理自己代碼的目的