LoginSignup
300
187

More than 5 years have passed since last update.

async-awaitでもforEachしたい!

Posted at

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レベルでハンドリングしたほうが便利なこともあります。

300
187
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
300
187