Posted at

async-awaitでもforEachしたい!

More than 1 year has passed since last update.

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は単にsetTimeoutPromise化しているだけで、そしてsleptLogsleepを使ってasync-awaitconsole.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されることもなく進んでしまいます。


シンプルなやり方には罠がある

関数を切らない、単なるforfor-infor-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レベルでハンドリングしたほうが便利なこともあります。