📌

JavaScriptのforEachで非同期処理を逐次実行する方法

2022/07/14に公開約2,400字1件のコメント

JavaScript の forEach は非同期ではない

時々ネットの解説記事で forEach は非同期だという解説を見かけますがこれは間違っています。実際の処理は逐次関数をコールバックしていくだけなので、普通に同期で動いています。コールバック中に非同期処理を使って待機動作を行っていないので、バラバラに動いているように見えるだけなのです。

検証用の非同期処理を作成

ランダムに 1000ms 以内の時間を待機して文字列を出力する関数です。TypeScript になっているので、JavaScript で実行したい場合は型定義を外してください。

const f = (value: string) => {
  return new Promise<void>((resolve) =>
    setTimeout(() => {
      console.log(value);
      resolve();
    }, Math.random() * 1000))
  );
};

普通に forEach で非同期処理を呼び出した場合

["A", "B", "C", "D", "E"].forEach((v) => {
  f(v);
});
console.log("終了");
終了
B
E
D
A
C

終了と表示した A~E がランダムに一気に出力されます。何も同期していません。

非同期関数を forEach に設定した場合

["A", "B", "C", "D", "E"].forEach(async (v) => {
  await f(v);
});
console.log("終了");
終了
D
A
B
E
C

コールバック関数を非同期にしても、結果は変わりません。非同期になるタイミングが異なるだけです。

Promise.all と map の組み合わせ

よくあるヤツです。

const main = async () => {
  await Promise.all(["A", "B", "C", "D", "E"].map((v) => f(v)));
  console.log("終了");
};
main();
B
E
A
C
D
終了

一応、終了が最後に来るようになりました。ただ、逐次実行はされていません。

for を使う

const main = async () => {
  for (const i of ["A", "B", "C", "D", "E"]) await f(i);
  console.log("終了");
};
main();
A
B
C
D
E
終了

逐次実行になりました。動作としてはこれで解決です。

forEach で逐次実行

本題に入ります。

let p = Promise.resolve();
["A", "B", "C", "D", "E"].forEach((v) => (p = p.then(() => f(v))));
p.then(() => console.log("終了"));
A
B
C
D
E
終了

ということで逐次実行に成功しました。
やっていることは簡単で逐次実行されるように Promise の then を繋げています。

forEach で逐次実行と await

逐次実行 の終了を await で待ちたいときは以下のようになります。

const main = async () => {
  let p = Promise.resolve();
  ["A", "B", "C", "D", "E"].forEach((v) => (p = p.then(() => f(v))));
  await p;
  console.log("終了");
};
main();
A
B
C
D
E
終了

reduce を使った方法

変数を外にお漏らししたくない場合は reduce を使います。

["A", "B", "C", "D", "E"]
  .reduce((p, v) => p.then(() => f(v)), Promise.resolve())
  .then(() => console.log("終了"));
A
B
C
D
E
終了

reduce と await

逐次実行 の終了を await で待ちたいときは以下のようになります。

const main = async () => {
  await ["A", "B", "C", "D", "E"].reduce(
    (p, v) => p.then(() => f(v)),
    Promise.resolve()
  );
  console.log("終了");
};
main();
A
B
C
D
E
終了

まとめ

見やすさを考えると for 文を使うのが一番良い気がしますが、forEach や reduce で繋ぎたい時は今回の方法が使えます。

セマフォで同期

セマフォで同期する記事を追加しました。

https://zenn.dev/sora_kumo/articles/0b55c12f1f6fec
GitHubで編集を提案

Discussion

ちょうどforEachの非同期処理で逐次処理にならず悩んでいたので勉強になりました!

ログインするとコメントできます