for 循環(huán)里的 await

之前有篇我的 blog 提到過 js 的異步發(fā)展史:從 callbackpromise 再到 async/awaitasync/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艳狐。碼字不易,歡迎點贊皿桑。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末毫目,一起剝皮案震驚了整個濱河市蔬啡,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌镀虐,老刑警劉巖箱蟆,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異粉私,居然都是意外死亡顽腾,警方通過查閱死者的電腦和手機近零,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門诺核,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人久信,你說我怎么就攤上這事窖杀。” “怎么了裙士?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵入客,是天一觀的道長。 經(jīng)常有香客問我腿椎,道長桌硫,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任啃炸,我火速辦了婚禮铆隘,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘南用。我一直安慰自己膀钠,他們只是感情好,可當我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布裹虫。 她就那樣靜靜地躺著肿嘲,像睡著了一般。 火紅的嫁衣襯著肌膚如雪筑公。 梳的紋絲不亂的頭發(fā)上雳窟,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天,我揣著相機與錄音匣屡,去河邊找鬼封救。 笑死,一個胖子當著我的面吹牛耸采,可吹牛的內(nèi)容都是我干的兴泥。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼虾宇,長吁一口氣:“原來是場噩夢啊……” “哼搓彻!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤旭贬,失蹤者是張志新(化名)和其女友劉穎怔接,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體稀轨,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡扼脐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了奋刽。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瓦侮。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖佣谐,靈堂內(nèi)的尸體忽然破棺而出肚吏,到底是詐尸還是另有隱情,我是刑警寧澤狭魂,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布罚攀,位于F島的核電站,受9級特大地震影響雌澄,放射性物質(zhì)發(fā)生泄漏斋泄。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一镐牺、第九天 我趴在偏房一處隱蔽的房頂上張望炫掐。 院中可真熱鬧,春花似錦任柜、人聲如沸卒废。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽摔认。三九已至,卻和暖如春宅粥,著一層夾襖步出監(jiān)牢的瞬間参袱,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工秽梅, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留抹蚀,地道東北人。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓企垦,卻偏偏與公主長得像环壤,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子钞诡,可洞房花燭夜當晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容