JavaScript界で非同期処理の切り札的存在となっているasync
-await
、そして軽やかにループ処理を行っていくforEach
。ただ、この2つを合わせて使おうとしたら、うまくいきませんでした。
素直に書いてみる
準備
まずは、非同期実行するもののモデルとして、こんなコードを書いてみます。
const sleep = time => new Promise(resolve => setTimeout(resolve, time));
const sleptLog = async val => {
await sleep(1000);
console.log('sleptLog', val);
};
sleep
は単にsetTimeout
をPromise
化しているだけで、そしてsleptLog
はsleep
を使ってasync
-await
でconsole.log
を遅らせる、というような処理です。
文法的な問題に
次に、これをforEach
で使ってみましょう。
const arr = [1, 2, 3];
const testFunc = async () => {
arr.forEach(item => await sleptLog(item));
console.log('done!')
};
こんなふうに書きたくなるかもしれませんが、これは文法エラーです。await
は、async
な関数の中に直接書くことしかできないのです。
書き換えても意図通りにはならず
ということで、forEach
のコールバックもasync
にしてみます。
const testFunc = async () => {
arr.forEach(async item => await sleptLog(item));
console.log('done!')
};
とりあえず文法エラーは解決したのですが、これを実行するとdone!
のほうが先に出力されてしまいます。forEach
の前にawait
を付けても同じです。
原因と対策
原因
forEach
は何が来ようが、コールバックの返り値を無視します。結果、async
関数が生成したPromise
も無視されて、await
されることもなく進んでしまいます。
シンプルなやり方には罠がある
関数を切らない、単なるfor
やfor-in
、for-of
などであればawait
はされます。
const testFunc = async () => {
for(let item of arr) await sleptLog(item);
console.log('done!')
};
ただ、実際に実行してみると気づくかと思いますが、1つ目の実行が終わってから2つ目のタイマーが始まる、というように直列的な動きとなります。あえて使う分には便利な場面もあるかとは思いますが、通信など並列で実行したいという場合には、この方法は向きません。
ライブラリに、あったじゃない!
「複数のPromise
を並列に走らせる」関数といえば、Promise.all
が標準で用意されています。今度は、これを使ってみましょう。
const testFunc = async () => {
await Promise.all(arr.map(async item => await sleptLog(item)))
console.log('done!')
};
配列をmap
して要素をPromise
に変換して、それをPromise.all
に投げ込むことで、「配列の中身すべてについてresolveまで待たせる」ことが実現できました。
まとめ
async
-await
も中身はPromise
です。状況によっては、Promise
レベルでハンドリングしたほうが便利なこともあります。