javascriptでforEachの時await(Promise・非同期処理)する。





node.jsで非同期プログラムを書いていた時に、2重のforEach内の処理が終わってからresolveしたいと思っていたのですが、意図通りの挙動にならなかったので、忘れないうちに自分用に解決方法をメモしておこうと思います。

上手くいかないパターン

(async () => {

  // サンプル配列
  const list1 = [{
    'name0': `value${Math.random()}`
  }, {
    'name1': `value${Math.random()}`
  }, {
    'name2': `value${Math.random()}`
  }, {
    'name3': `value${Math.random()}`
  }, ];

  const list2 = [{
    'fruit0': 'apple'
  }, {
    'fruit1': 'cherry'
  }, {
    'fruit2': 'banana'
  }, ];

  console.log('スクリプト開始...');
  await forEachLoopPromiseTest(list1, list2);
  console.log('スクリプト完了!');


  // ForEachを使う時は注意が必要
  async function forEachLoopPromiseTest(list1, list2) {
    return new Promise(async (resolve, reject) => {
      list1.forEach(async (list1_data) => {
        list2.forEach(async (list2_data) => {

          // ※何かAwaitな処理
          // ※何かAwaitな処理
          // ※何かAwaitな処理

        });
        console.log(`list2.forEachが完了\n`);
      });
      console.log(`list1.forEachが完了\n`);
      // ループ内のすべての処理が完了したらresolve
      resolve()
    });
  }

})();

これだと、list2.forEachブロック内の処理が全部終わる前に関数「forEachLoopPromiseTest」がresolveしてしまって、「スクリプト完了!」って出た後で「list2.forEachが完了」だの「list1.forEachが完了」が表示されてしまいます。(本来は両方のforEachが完了してから「スクリプト完了!」となる様に実行したい)

解決のポイント

解決のポイントとしては、以下の3つです。

  • forEachは何も返さないのでawaitしても無駄。(await list1.forEach()…などと書いても待ってくれない。Promiseが返ってこない。)
  • Promise.all()を使う。
  • list2.forEach内に即時実行関数を作成して配列に代入→Promise.all()で確認。

技術的用語

ややこしいのでググってください…

  • Promise.all = Promise.all([PromiseObjectArray]).then(()=>{ // 全部終わったときの処理 })
  • 即時実行関数 = すぐ実行する関数。 ( () => { // ここに処理→Promiseを返す } )();
  • 関数は配列に代入可能。

上手く行ったコード

要するにPromiseを返す関数の中に配列を用意して、Promiseオブジェクトを返す即時実行関数をその配列に代入→Promise.all()で確認して、全部完了したら関数をresolveする。

(async () => {

  // サンプル配列
  const list1 = [{
    'name0': `value${Math.random()}`
  }, {
    'name1': `value${Math.random()}`
  }, {
    'name2': `value${Math.random()}`
  }, {
    'name3': `value${Math.random()}`
  }, ];

  const list2 = [{
    'fruit0': 'apple'
  }, {
    'fruit1': 'cherry'
  }, {
    'fruit2': 'banana'
  }, ];

  console.log('スクリプト開始...');
  await forEachLoopPromiseTest(list1, list2);
  console.log('スクリプト完了!');


  // ForEachを使う時は注意が必要
  async function forEachLoopPromiseTest(list1, list2) {
    return new Promise(async (resolve, reject) => {
      // promise配列。これが全部resolveしたら完了
      let promiseArray = [];
      list1.forEach(async (list1_data) => {
        console.log(`list1.forEach:${JSON.stringify(list1_data)}`);
        list2.forEach(async (list2_data) => {
          console.log(`list2.forEach:${JSON.stringify(list2_data)}`);
          // 即時実行関数を作ってそれをPromiseArrayに代入
          promiseArray.push((() => {
            return new Promise(async (resolve, reject) => {

              // ※何かAwaitな処理
              // ※何かAwaitな処理
              // ※何かAwaitな処理

              // 即時実行関数がresolve()
              console.log(`即時実行関数がresolve()\n`);
              resolve();
            });
          })());
        });
        console.log(`list2.forEachが完了\n`);
      });
      console.log(`list1.forEachが完了\n`);
      // ループ内のすべての処理が完了したらresolve
      Promise.all(promiseArray).then(() => {
        console.log(`promiseArray内のすべての即時実行関数がresolve()\n`);
        resolve()
      });
    });
  }

})();

反省点

まあぶっちゃけネストがややこしい。読みにくい。意図通りには動くのですがもっとすっきりかける方法があるかもしれません。

例えばうまいこと切り分けで別の関数にするとか。上手いこと改善できそうならまた更新します。


質問・コメントなどあると嬉しいです