之前有篇我的 blog 提到過 js 的異步發(fā)展史:從 callback
到 promise
再到 async/await
。async/await
之后的 JS 開始允許我們以一種看似順序執(zhí)行的方式書寫代碼培廓,這讓入門 JS 變得更簡單肢预,但在一些復雜的場景里比如 for-loop 環(huán)境里臀防,async/await
還是會有不少坑的涛救。
Warm up
開始前牧嫉,先寫個“菜籃子工程”剂跟,getVegetableNum
是本文中最基礎(chǔ)的一個異步函數(shù)——異步獲取蔬菜數(shù)量:
const Basket = {
onion: 1,
ginger: 2,
garlic: 3,
}
const getVegetableNum = async (veg) => Basket[veg];
知識點:async
是一個語法糖,表示把結(jié)果包在 Promise 里返回酣藻;該異步函數(shù)等價于
function getVegetableNum (veg) {
return Promise.resolve( Basket[veg] );
}
OK曹洽,我們再試著異步獲取三種蔬菜的數(shù)量:
const start1 = async () => {
console.log('Start');
const onion = await getVegetableNum('onion');
console.log('onion', onion);
const ginger = await getVegetableNum('ginger');
console.log('ginger', ginger);
const garlic = await getVegetableNum('garlic');
console.log('garlic', garlic);
console.log('End');
}
最后打印結(jié)果如下:
Start
onion 1
ginger 2
garlic 3
End
await in a for loop
OK,前言到此為止×删纾現(xiàn)實中開發(fā)中送淆,上述代碼枚舉每一種蔬菜的方式太過冗余,一般我們更傾向于寫個循環(huán)來調(diào)用 getVegetableNum
方法:
const start = async () => {
console.log('Start');
const arr = ['onion', 'ginger', 'garlic'];
for(let i = 0; i < arr.length; ++i>){
const veg = arr[i];
const num = await getVegetableNum(veg);
console.log(veg, num);
}
console.log('End');
}
結(jié)果依舊怕轿,這說明在普通的 for 循環(huán)里偷崩,程序會等待上一步迭代結(jié)束執(zhí)行 await 后,再繼續(xù)下一步迭代撞羽。這個和我們的預期一致阐斜,for 循環(huán)里的 async/await 是順序執(zhí)行的;同理也適用于 while放吩、for-in智听、for-of 等等形式中。
Start
onion 1
ginger 2
garlic 3
End
await in callback loop
不過渡紫,for 循環(huán)還有可以寫成其他形式到推,如 forEach、map惕澎、reduce莉测、filter 等等,這些需要 callback(回調(diào)方法)的循環(huán)唧喉,似乎就不那么好理解了捣卤。
forEach
我們試著用 forEach 代替上面的 for-loop 代碼:
const start = async () => {
console.log('Start');
['onion', 'ginger', 'garlic']
.forEach(async function callback(veg){
const num = await getVegetableNum(veg);
console.log(veg, num);
});
console.log('End');
}
看下方的輸出結(jié)果:顯然亂了八孝,End
比預期更早出現(xiàn)了董朝。原因很簡單,async/await
只是一種語法糖干跛,而 forEach 并非 promise-aware 語法子姜,它的 transform&compile 是有問題的:callback 直接返回了第一個 await 后的 Promise,而之后的判定楼入,被放在了下一個 tick 里哥捕。
Start
End
onion 1
ginger 2
garlic 3
map
使用 map 來觀察 callback 會更加直觀:
const start = async () => {
console.log('Start');
const promises = ['onion', 'ginger', 'garlic']
.map(async function callback(veg) {
const num = await getVegetableNum(veg);
console.log(veg, num);
});
console.log('promises:', promises);
console.log('End');
}
小改了一下代碼,map 執(zhí)行結(jié)果和 forEach 如出一轍嘉熊;看下方的打印結(jié)果:執(zhí)行完 map 后返回的是一個 Pending 狀態(tài)的 Promise 數(shù)組遥赚;而 await 之后的判定,在下一個 microTask 里執(zhí)行(MiroTask 分析見《MacroTask & MicroTask》)
Start
promises: [ Promise { <pending> }, Promise { <pending> }, Promise { <pending> } ]
End
onion 1
ginger 2
garlic 3
filter
再看看 filter阐肤,callback 的返回事實上也是一個 Promise凫佛,而 Promise 在條件判斷時為 true,所以這種情況下 filter 的判斷永遠為真孕惜,所以只淺拷貝了一份數(shù)組而已愧薛。
const moreThan1 = ['onion', 'ginger', 'garlic']
.filter(async (veg) => {
const num = await getVegetableNum(veg);
return num > 1;
});
//moreThan1 = ['onion', 'ginger', 'garlic']
reduce
最后還有 reduce,下面代碼里的 sum 返回的也是 Promise:但它的 callback 和上述的幾個方法還不一樣诊赊,竟然是 promise-aware 的(別問我為什么厚满,就是這么規(guī)定的!)
const sum = ['onion', 'ginger', 'garlic']
.reduce(async (acc, veg) => {
const num = await getVegetableNum(veg);
return acc + num;
}, 0);
console.log(sum); // Promise { <pending> }
console.log(await sum); // [object Promise]3
我們看看 sum 這個 promise 的判定結(jié)果是[object Promise]3
碧磅,很有趣吧碘箍。稍微分析一下:
- 在第一次迭代時,callback 里的
acc
是 0——初始值鲸郊,num
是 1丰榴,acc+num
是 2,但由于是 async 函數(shù)秆撮,返回的是一個 Promise(上面提到過) - 第二個迭代開始四濒,
acc
就一直是 Promise 了,而 Promise+num
的打印結(jié)果是[object Promise]${num}
- 最后一個迭代的
num
是 3, 所以返回的sum
也就成了Promise{ '[object Promise]3' }
reduce 既然是 promise-aware 語法盗蟆,所以它的問題比上面三個好解決:acc
不是 Promise 嗎戈二?直接利用 await 返回 acc
判定結(jié)果就是了:
const sum = await ['onion', 'ginger', 'garlic']
.reduce(async (acc, veg) => {
const num = await getVegetableNum(veg);
return (await acc) + num;
}, 0);
console.log(sum); // 6
當然這個寫法確實挺難看的。
Promise.all
我們看了上面四種迭代方法——forEach喳资、map觉吭、filter、reduce仆邓,只要是 callback 使用了async/await
鲜滩,結(jié)果就不是很靠譜了,所以應該盡量避免這種寫法节值。那怎么改寫呢徙硅?可以先把所有異步數(shù)據(jù)一次性取過來,再進行后續(xù)循環(huán)操作搞疗;批量取數(shù)據(jù)常用的手段就是 Promise.all
:
const fetchNums = (vegs) => {
const promises = vegs.map( getVegetableNum );
return Promise.all( promises );
}
const start = async () => {
console.log('Start');
const nums = await fetchNums( ['onion', 'ginger', 'garlic'] );
console.log(nums); // [1, 2, 3]
// then map, forEach, filter or reduce according to nums
console.log('End');
}
好處還是挺明顯的:
- 從代碼質(zhì)量上來說嗓蘑,符合單一原則,將取數(shù)據(jù)和操作數(shù)據(jù)分開來
- 從性能上來說贴汪,循環(huán)里的異步請求是順序執(zhí)行的脐往,而
Promise.all
是并發(fā)執(zhí)行的,速度更快
小結(jié)
今天回顧了 async/await
在循環(huán)語句里的使用方法扳埂,對于普通的 for-loop业簿,所有的 await 都是串行調(diào)用的,可以放心使用阳懂,包括 while梅尤、for-in、for-of 等等岩调;但是在有 callback 的 array 方法巷燥,如 forEach、map号枕、filter缰揪、reduce 等等,有許多副作用葱淳,最好就別使用 await 了钝腺。當然最優(yōu)解還是 Promise.all
,無論從質(zhì)量上還是效率上都是不二選擇赞厕。
相關(guān)
文章同步發(fā)布于an-Onion 的 Github艳狐。碼字不易,歡迎點贊皿桑。