全文共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
如需轉載,請後臺留言,遵守轉載規範