自從Node的7.6版本,已經預設支援async/await特性了。如果你還沒有使用過他,或者對他的用法不太瞭解,這篇文章會告訴你為什麼這個特性“不容錯過”。本文輔以大量例項,相信你能很輕鬆的看懂,並瞭解Javascript處理非同步的一大殺器。
文章靈感和內容借鑑了6 Reasons Why JavaScript’s Async/Await Blows Promises Away (Tutorial),英文好的同學可以直接戳原版參考。
初識Async/await
對於還不瞭解Async/await特性的同學,下面一段是一個“速成”培訓。
Async/await 是Javascript編寫非同步程式的新方法。以往的非同步方法無外乎回撥函式和Promise。但是Async/await建立於Promise之上。對於Javascript處理非同步,是個老生常談卻歷久彌新的話題:
從最早的回撥函式,到 Promise 物件,再到 Generator 函式,每次都有所改進,但又讓人覺得不徹底。它們都有額外的複雜性,都需要理解抽象的底層執行機制。
非同步程式設計的最高境界,就是根本不用關心它是不是非同步。
async 函式就是隧道盡頭的亮光,很多人認為它是非同步操作的終極解決方案。
Async/await語法
試想一下,我們有一個getJSON方法,該方法傳送一個非同步請求JSON資料,並返回一個promise物件。這個promise物件的resolve方法傳遞非同步獲得的JSON資料。具體例子的使用如下:
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關鍵字。在函式體內,使用了await關鍵字。當然await關鍵字只能出現在用async宣告的函式體內。該函式會隱式地返回一個Promise物件,函式體內的return值,將會作為這個Promise物件resolve時的引數。
可以使用then方法新增回撥函式。當函式執行的時候,一旦遇到await就會先返回,等到非同步操作完成,再接著執行函式體內後面的語句。
2)示例中,await getJSON() 說明console.log的呼叫,會等到getJSON()返回的promise物件resolve之後觸發。
我們在看一個例子加強一下理解,該例子取自阮一峰大神的《ECMAScript 6 入門》一書:
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value);
}
asyncPrint(`hello world`, 50);
上面程式碼指定50毫秒以後,輸出hello world。
Async/await究竟好在哪裡?
那麼,同樣是處理非同步操作,Async/await究竟好在哪裡呢?
我們總結出以下6點。
簡約而乾淨Concise and clean
我們看一下上面兩處程式碼的程式碼量,就可以直觀地看出使用Async/await對於程式碼量的節省是很明顯的。對比Promise,我們不需要書寫.then,不需要新建一個匿名函式處理響應,也不需要再把資料賦值給一個我們其實並不需要的變數。同樣,我們避免了耦合的出現。這些看似很小的優勢其實是很直觀的,在下面的程式碼示例中,將會更加放大。
錯誤處理Error handling
Async/await使得處理同步+非同步錯誤成為了現實。我們同樣使用try/catch結構,但是在promises的情況下,try/catch難以處理在JSON.parse過程中的問題,原因是這個錯誤發生在Promise內部。想要處理這種情況下的錯誤,我們只能再巢狀一層try/catch,就像這樣:
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處理,一切變得簡單,解析中的錯誤也能輕而易舉的解決:
const makeRequest = async () => {
try {
// this parse may fail
const data = JSON.parse(await getJSON())
console.log(data)
}
catch (err) {
console.log(err)
}
}
條件判別Conditionals
想象一下這樣的業務需求:我們需要先拉取資料,然後根據得到的資料判斷是否輸出此資料,或者根據資料內容拉取更多的資訊。如下:
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層)巢狀過程中,非常容易“丟失自我”。
使用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
}
}
中間值Intermediate values
一個經常出現的場景是,我們先調起promise1,然後根據返回值,呼叫promise2,之後再根據這兩個Promises得值,調取promise3。使用Promise,我們不難實現:
const makeRequest = () => {
return promise1()
.then(value1 => {
// do something
return promise2(value1)
.then(value2 => {
// do something
return promise3(value1, value2)
})
})
}
如果你難以忍受這樣的程式碼,我們可以優化我們的Promise,方案是使用Promise.all來避免很深的巢狀。
就像這樣:
const makeRequest = () => {
return promise1()
.then(value1 => {
// do something
return Promise.all([value1, promise2(value1)])
})
.then(([value1, value2]) => {
// do something
return promise3(value1, value2)
})
}
Promise.all這個方法犧牲了語義性,但是得到了更好的可讀性。
但是其實,把value1 & value2一起放到一個陣列中,是很“蛋疼”的,某種意義上也是多餘的。
同樣的場景,使用async/await會非常簡單:
const makeRequest = async () => {
const value1 = await promise1()
const value2 = await promise2(value1)
return promise3(value1, value2)
}
錯誤堆疊資訊Error stacks
想象一下我們鏈式呼叫了很多promises,一級接一級。緊接著,這條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)
})
此鏈條的錯誤堆疊資訊並沒用線索指示錯誤到底出現在哪裡。更糟糕的事,他還會誤導開發者:錯誤資訊中唯一出現的函式名稱其實根本就是無辜的。
我們再看一下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)
})
也許這樣的對比,對於在本地開發階段區別不是很大。但是想象一下在伺服器端,線上程式碼的錯誤日誌情況下,將會變得非常有意義。你一定會覺得上面這樣的錯誤資訊,比“錯誤出自一個then的then的then。。。”有用的多。
除錯Debugging
最後一點,但是也是很重要的一點,使用async/await來debug會變得非常簡單。
在一個返回表示式的箭頭函式中,我們不能設定斷點,這就會造成下面的局面:
const makeRequest = () => {
return callAPromise()
.then(()=>callAPromise())
.then(()=>callAPromise())
.then(()=>callAPromise())
.then(()=>callAPromise())
}
我們無法在每一行設定斷點。但是使用async/await時:
const makeRequest = async () => {
await callAPromise()
await callAPromise()
await callAPromise()
await callAPromise()
}
總結
Async/await是近些年來JavaScript最具革命性的新特性之一。他讓讀者意識到使用Promise存在的一些問題,並提供了自身來代替Promise的方案。
當然,對這個新特性也有一定的擔心,體現在:
他使得非同步程式碼變的不再明顯,我們好不容易已經學會並習慣了使用回撥函式或者.then來處理非同步。新的特性當然需要時間成本去學習和體會;
退回來說,熟悉C#語言的程式設計師一定會懂得這些學習成本是完全值得的。
Happy Coding!
PS: 作者Github倉庫,歡迎通過程式碼各種形式交流。