forEach と async function

forEach と async function:

先日、API Gateway + Lambda な環境で JavaScript のコード内で Array#forEach に渡した async function が実行されずにハマってたので書いておきます。


TL;DR


  • Array#forEachasync function は渡さない方がいい

  • Promise.allArray#map を組み合わせると良さげ


現象

for 文は冗長な気がして、Array#forEachArray#map を使いたい今日この頃。

配列に対して返り値を必要としない処理を行う場合、Array#forEach を利用したくなりますが、 async function を渡した場合にうまくいきません。

例えば、以下のコードを実行します。

foreach0.ts
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); 
 
const asyncFunc = async (sec: number, idx: number) => { 
  await sleep(sec * 1000); 
  console.log(`sleep[${idx}]: ${sec} sec`); 
}; 
 
const a = [4, 3, 2, 1]; 
a.forEach(asyncFunc); 
console.log('finish'); 
$ ts-node foreach0.ts 
finish 
sleep[3]: 1sec 
sleep[2]: 2sec 
sleep[1]: 3sec 
sleep[0]: 4sec 
forEach 内の処理が終わる前に、finish が表示されています。

これは forEach が返り値を全く考慮しない、たとえ Promise が帰って来ようが処理を進めてしまうのが原因です。


対策

対策方法は、処理を順番に行いたい場合と、並列に行いたい場合で異なります。

  1. 順番に行いたい場合
配列の要素を順番に処理したい場合は for 文を使いましょう。

foreach1.ts
type AsyncFunc<T> = (x: T, i: number) => Promise<void>; 
 
const asyncForEach = async <T>(ary: T[], fn: AsyncFunc<T>) => { 
  for (let i = 0; i < ary.length; i = i + 1) { 
    await fn(ary[i], i); 
  } 
}; 
 
const main = async () => { 
  const a = [4, 3, 2, 1]; 
  await asyncForEach(a, asyncFunc); 
  console.log('finish'); 
}; 
 
main(); 
$ ts-node foreach1.ts 
sleep[0]: 4 sec 
sleep[1]: 3 sec 
sleep[2]: 2 sec 
sleep[3]: 1 sec 
finish 
配列の順番に処理が実行されています。

インデックスが不要な場合は for..of 文を利用してもいいでしょう。

  1. 並列に行いたい場合
配列の順番に関係なく、並列に処理したい場合は Promise.allArray#map を組み合わせます。

foreach2.ts
const parallelForEach = <T>(ary: T[], fn: AsyncFunc<T>) => ( 
  Promise.all(ary.map((x, i) => fn(x, i))) 
); 
 
const main = async () => { 
  const a = [4, 3, 2, 1]; 
  await parallelForEach(a, asyncFunc); 
  console.log('finish'); 
}; 
 
main(); 
$ ts-node foreach2.ts 
sleep[3]: 1 sec 
sleep[2]: 2 sec 
sleep[1]: 3 sec 
sleep[0]: 4 sec 
finish 
並列に処理が実行されています。


まとめ


  • Array#forEachasync function を待ってくれない
  • 順番に処理したい場合は for 文を使う
  • 並列に処理したい場合は Promise.allArray#map を組み合わせて使う
ちなみにハマった件では Promise.allArray#map の方で何とかしました。

コメント

このブログの人気の投稿

投稿時間:2021-06-17 22:08:45 RSSフィード2021-06-17 22:00 分まとめ(2089件)

投稿時間:2021-06-20 02:06:12 RSSフィード2021-06-20 02:00 分まとめ(3871件)

投稿時間:2021-06-17 05:05:34 RSSフィード2021-06-17 05:00 分まとめ(1274件)