すべての非同期forEachコールバックが完了した後のコールバック


245

タイトルが示唆するように。どうすればよいですか?

whenAllDone()forEachループが各要素を通過し、いくつかの非同期処理を実行した後に呼び出します。

[1, 2, 3].forEach(
  function(item, index, array, done) {
     asyncFunction(item, function itemDone() {
       console.log(item + " done");
       done();
     });
  }, function allDone() {
     console.log("All done");
     whenAllDone();
  }
);

このように機能させることは可能ですか?forEachの2番目の引数が、すべての反復を通過したときに実行されるコールバック関数である場合、

予想される出力:

3 done
1 done
2 done
All done!

13
標準の配列forEachメソッドにdoneコールバックパラメータとallDoneコールバックがあると便利です。
Vanuan

22
それは本当に単純なものであり、とても単純なのでJavaScriptで多くのレスリングが必要です。
2016

回答:


410

Array.forEach この機能は提供されていません(提供されている場合でも)。ただし、目的を達成する方法はいくつかあります。

単純なカウンターの使用

function callback () { console.log('all done'); }

var itemsProcessed = 0;

[1, 2, 3].forEach((item, index, array) => {
  asyncFunction(item, () => {
    itemsProcessed++;
    if(itemsProcessed === array.length) {
      callback();
    }
  });
});

(@vanuanなどに感謝)このアプローチは、「完了」コールバックを呼び出す前にすべてのアイテムが処理されることを保証します。コールバックで更新されるカウンターを使用する必要があります。非同期操作の戻り順序は保証されないため、indexパラメーターの値によっては同じ保証が得られません。

ES6 Promiseの使用

(promiseライブラリは古いブラウザで使用できます):

  1. 同期実行を保証するすべてのリクエストを処理します(例:1、2、3)

    function asyncFunction (item, cb) {
      setTimeout(() => {
        console.log('done with', item);
        cb();
      }, 100);
    }
    
    let requests = [1, 2, 3].reduce((promiseChain, item) => {
        return promiseChain.then(() => new Promise((resolve) => {
          asyncFunction(item, resolve);
        }));
    }, Promise.resolve());
    
    requests.then(() => console.log('done'))
    
  2. 「同期」実行なしですべての非同期要求を処理します(2は1より速く終了する場合があります)

    let requests = [1,2,3].map((item) => {
        return new Promise((resolve) => {
          asyncFunction(item, resolve);
        });
    })
    
    Promise.all(requests).then(() => console.log('done'));
    

非同期ライブラリの使用

他にも非同期ライブラリがあり、非同期が最も人気があり、必要なものを表現するメカニズムを提供します。

編集する

質問の本文は、以前に同期したサンプルコードを削除するように編集されているため、明確にするために回答を更新しました。元の例では、同期のようなコードを使用して非同期の動作をモデル化していたため、以下が適用されます。

array.forEach同期であり、したがってなのでres.write、foreachの呼び出しの後にコールバックを配置するだけです。

  posts.foreach(function(v, i) {
    res.write(v + ". index " + i);
  });

  res.end();

31
ただし、forEach内に非同期のものがある場合(たとえば、URLの配列をループしてHTTP GETを実行している場合)、res.endが最後に呼び出される保証はありません。
AlexMA 2013

ループで非同期アクションが実行された後にコールバックを起動するには、非同期ユーティリティの各メソッドを使用できます。github.com
caolan

2
@Vanuan私はあなたのかなり重要な編集によりよく一致するように私の答えを更新しました:)
Nick Tomlin

4
なぜだけif(index === array.length - 1)ではなく削除itemsProcessed
Amin Jafari 2016年

5
@AminJafariは、非同期呼び出しが登録された正確な順序で解決されない可能性があるためです(たとえば、サーバーを呼び出しており、2番目の呼び出しでわずかに停止しますが、最後の呼び出しは正常に処理されます)。最後の非同期呼び出しは、前の非同期呼び出しの前に解決できます。解決する順序に関係なく、すべてのコールバックを起動する必要があるため、カウンターを変更すると、これを防ぐことができます。
Nick Tomlin 2016年

25

非同期関数があり、コードを実行する前にそのタスクが完了することを確認したい場合は、コールバック機能を常に使用できます。

例えば:

var ctr = 0;
posts.forEach(function(element, index, array){
    asynchronous(function(data){
         ctr++; 
         if (ctr === array.length) {
             functionAfterForEach();
         }
    })
});

注:functionAfterForEachは、foreachタスクの完了後に実行される関数です。 asynchronousforeach内で実行される非同期関数です。


9
非同期リクエストの実行順序は保証されていないため、これは機能しません。最後の非同期リクエストは他のリクエストより先に終了し、すべてのリクエストが完了する前にfunctionAfterForEach()を実行する可能性があります。
レミー・デイヴィッド2015年

@RémyDAVIDうん、実行の順序に関してポイントがあります。または、プロセスが完了するまでの時間を言いますが、JavaScriptはシングルスレッドなので、これは最終的に機能します。そして証明は、この回答が受け取った賛成票です。
EmilReñaEnriquez

1
なぜ多くの賛成票があるのか​​よくわかりませんが、レミは正しいです。非同期とは、要求がいつでも返される可能性があるため、コードはまったく機能しません。JavaScriptはマルチスレッドではありませんが、ブラウザはマルチスレッドです。重く、追加するかもしれません。したがって、サーバーから応答を受信したタイミングに応じて、コールバックのいずれかをいつでもどのような順序でも呼び出すことができます...
Alexis Wilke

2
ええ、これは答えが完全に間違っています。10回のダウンロードを並行して実行した場合、最後のダウンロードが残りのダウンロードより先に完了し、実行が終了することはほぼ保証されています。
knrdk 16

カウンターを使用して、完了した非同期タスクの数を増やし、インデックスではなく配列の長さと照合することをお勧めします。賛成票の数は、回答の正しさの証明とは関係ありません。
アレックス

17

これで問題が解決することを願っています。通常、内部で非同期タスクを使用してforEachを実行する必要があるときにこれを使用します。

foo = [a,b,c,d];
waiting = foo.length;
foo.forEach(function(entry){
      doAsynchronousFunction(entry,finish) //call finish after each entry
}
function finish(){
      waiting--;
      if (waiting==0) {
          //do your Job intended to be done after forEach is completed
      } 
}

function doAsynchronousFunction(entry,callback){
       //asynchronousjob with entry
       callback();
}

私のAngular 9コードにも同様の問題がありましたが、この答えでうまくいきました。@EmilReñaEnriquezの回答も私には役立ちましたが、この問題の方がより正確で簡単な答えだと思います。
オモスタン

17

非同期の場合に不正解がいくつ出されたのかは奇妙です。インデックスをチェックしても期待される動作が提供されないことを簡単に示すことができます。

// INCORRECT
var list = [4000, 2000];
list.forEach(function(l, index) {
    console.log(l + ' started ...');
    setTimeout(function() {
        console.log(index + ': ' + l);
    }, l);
});

出力:

4000 started
2000 started
1: 2000
0: 4000

を確認するとindex === array.length - 1、最初の要素がまだ保留されている間に、最初の反復が完了するとコールバックが呼び出されます。

非同期などの外部ライブラリを使用せずにこの問題を解決するには、リストの長さを節約し、各反復後に減少することが最善の策だと思います。スレッドが1つしかないため、競合状態が発生する可能性はありません。

var list = [4000, 2000];
var counter = list.length;
list.forEach(function(l, index) {
    console.log(l + ' started ...');
    setTimeout(function() {
        console.log(index + ': ' + l);
        counter -= 1;
        if ( counter === 0)
            // call your callback here
    }, l);
});

1
それがおそらく唯一の解決策です。非同期ライブラリもカウンターを使用しますか?
Vanuan

1
他のソリューションでも機能しますが、チェーンや複雑さを追加する必要がないため、これは最も説得力があります。キッス
アザタール2017

配列の長さがゼロの場合も考慮してください。この場合、コールバックは呼び出されません
Saeed Ir

6

ES2018では、非同期イテレータを使用できます。

const asyncFunction = a => fetch(a);
const itemDone = a => console.log(a);

async function example() {
  const arrayOfFetchPromises = [1, 2, 3].map(asyncFunction);

  for await (const item of arrayOfFetchPromises) {
    itemDone(item);
  }

  console.log('All done');
}

1
ノードv10で使用可能
Matt Swezey

2

約束のない私の解決策(これにより、すべてのアクションが次のアクションが始まる前に確実に終了します):

Array.prototype.forEachAsync = function (callback, end) {
        var self = this;
    
        function task(index) {
            var x = self[index];
            if (index >= self.length) {
                end()
            }
            else {
                callback(self[index], index, self, function () {
                    task(index + 1);
                });
            }
        }
    
        task(0);
    };
    
    
    var i = 0;
    var myArray = Array.apply(null, Array(10)).map(function(item) { return i++; });
    console.log(JSON.stringify(myArray));
    myArray.forEachAsync(function(item, index, arr, next){
      setTimeout(function(){
        $(".toto").append("<div>item index " + item + " done</div>");
        console.log("action " + item + " done");
        next();
      }, 300);
    }, function(){
        $(".toto").append("<div>ALL ACTIONS ARE DONE</div>");
        console.log("ALL ACTIONS ARE DONE");
    });
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="toto">

</div>


1
 var counter = 0;
 var listArray = [0, 1, 2, 3, 4];
 function callBack() {
     if (listArray.length === counter) {
         console.log('All Done')
     }
 };
 listArray.forEach(function(element){
     console.log(element);
     counter = counter + 1;
     callBack();
 });

1
foreach内に非同期操作がある場合は機能しません。
Sudhanshu Gaur


0

私の解決策:

//Object forEachDone

Object.defineProperty(Array.prototype, "forEachDone", {
    enumerable: false,
    value: function(task, cb){
        var counter = 0;
        this.forEach(function(item, index, array){
            task(item, index, array);
            if(array.length === ++counter){
                if(cb) cb();
            }
        });
    }
});


//Array forEachDone

Object.defineProperty(Object.prototype, "forEachDone", {
    enumerable: false,
    value: function(task, cb){
        var obj = this;
        var counter = 0;
        Object.keys(obj).forEach(function(key, index, array){
            task(obj[key], key, obj);
            if(array.length === ++counter){
                if(cb) cb();
            }
        });
    }
});

例:

var arr = ['a', 'b', 'c'];

arr.forEachDone(function(item){
    console.log(item);
}, function(){
   console.log('done');
});

// out: a b c done

解決策は革新的ですが、エラーが発生しています-「タスクは機能ではありません」
Genius

0

私はそれを解決するために簡単な方法を試し、あなたとそれを共有します:

let counter = 0;
            arr.forEach(async (item, index) => {
                await request.query(item, (err, recordset) => {
                    if (err) console.log(err);

                    //do Somthings

                    counter++;
                    if(counter == tableCmd.length){
                        sql.close();
                        callback();
                    }
                });

requestノードjsのmssqlライブラリの関数です。これは、各関数または必要なコードを置き換えることができます。幸運を


0
var i=0;
const waitFor = (ms) => 
{ 
  new Promise((r) => 
  {
   setTimeout(function () {
   console.log('timeout completed: ',ms,' : ',i); 
     i++;
     if(i==data.length){
      console.log('Done')  
    }
  }, ms); 
 })
}
var data=[1000, 200, 500];
data.forEach((num) => {
  waitFor(num)
})

-2

リストを反復処理するためのコールバックは必要ありません。end()ループの後に呼び出しを追加するだけです。

posts.forEach(function(v, i){
   res.write(v + ". Index " + i);
});
res.end();

3
いいえ。OPは非同期ロジックが各反復で実行されることを強調しました。res.writeは非同期操作ではないため、コードは機能しません。
ジムG.

-2

簡単な解決策は次のようなものです

function callback(){console.log("i am done");}

["a", "b", "c"].forEach(function(item, index, array){
    //code here
    if(i == array.length -1)
    callback()
}

3
質問の前提である非同期コードでは機能しません。
grg 2018

-3

完全な反復回数をチェックするsetIntervalは保証をもたらしますか?スコープをオーバーロードしないかどうかはわかりませんが、私はそれを使用しているようです

_.forEach(actual_JSON, function (key, value) {

     // run any action and push with each iteration 

     array.push(response.id)

});


setInterval(function(){

    if(array.length > 300) {

        callback()

    }

}, 100);

これは論理的に単純に見えます
Zeal Murapa '19年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.