Rückruf, nachdem alle asynchronen forEach-Rückrufe abgeschlossen sind

245

Wie der Titel schon sagt. Wie mache ich das?

Ich möchte aufrufen, whenAllDone()nachdem die forEach-Schleife jedes Element durchlaufen und eine asynchrone Verarbeitung durchgeführt hat.

[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();
  }
);

Möglich, dass es so funktioniert? Wenn das zweite Argument für forEach eine Rückruffunktion ist, die ausgeführt wird, sobald alle Iterationen durchlaufen wurden?

Erwartete Ausgabe:

3 done
1 done
2 done
All done!
Dan Andreasson
quelle
13
Es wäre schön, wenn die Standard-Array- forEachMethode einen doneRückrufparameter und einen allDoneRückruf hätte!
Vanuan
22
Es ist eine echte Schande, dass etwas so Einfaches so viel Wrestling in JavaScript erfordert.
Ali

Antworten:

410

Array.forEach bietet diese Freundlichkeit nicht (oh, wenn es so wäre), aber es gibt verschiedene Möglichkeiten, um das zu erreichen, was Sie wollen:

Mit einem einfachen Zähler

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

var itemsProcessed = 0;

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

(Dank an @vanuan und andere) Dieser Ansatz garantiert, dass alle Elemente verarbeitet werden, bevor der Rückruf "erledigt" aufgerufen wird. Sie müssen einen Zähler verwenden, der im Rückruf aktualisiert wird. Abhängig vom Wert des Indexparameters wird nicht die gleiche Garantie gegeben, da die Reihenfolge der Rückgabe der asynchronen Operationen nicht garantiert ist.

ES6-Versprechen verwenden

(Eine Versprechensbibliothek kann für ältere Browser verwendet werden):

  1. Verarbeiten Sie alle Anforderungen, die eine synchrone Ausführung gewährleisten (z. B. 1, dann 2, dann 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. Verarbeiten Sie alle asynchronen Anforderungen ohne "synchrone" Ausführung (2 werden möglicherweise schneller als 1 beendet).

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

Verwenden einer asynchronen Bibliothek

Es gibt andere asynchrone Bibliotheken, von denen Async am beliebtesten ist und die Mechanismen bieten, um auszudrücken, was Sie wollen.

Bearbeiten

Der Hauptteil der Frage wurde bearbeitet, um den zuvor synchronen Beispielcode zu entfernen. Daher habe ich meine Antwort zur Verdeutlichung aktualisiert. Im ursprünglichen Beispiel wurde synchroner Code verwendet, um asynchrones Verhalten zu modellieren. Daher gilt Folgendes:

array.forEachist synchron und so ist es auch res.write, so dass Sie Ihren Rückruf einfach nach Ihrem Anruf an foreach senden können:

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

  res.end();
Nick Tomlin
quelle
31
Beachten Sie jedoch, dass es keine Garantie dafür gibt, dass res.end zuletzt aufgerufen wird, wenn sich in forEach asynchrones Material befindet (z. B. wenn Sie ein Array von URLs durchlaufen und ein HTTP-GET ausführen).
AlexMA
Um einen Rückruf auszulösen, nachdem eine asynchrone Aktion in einer Schleife ausgeführt wurde, können Sie die einzelnen Methoden des asynchronen Dienstprogramms verwenden: github.com/caolan/async#each
elkelk
2
@ Vanuan Ich habe meine Antwort aktualisiert, um besser zu Ihrer ziemlich wichtigen Bearbeitung zu passen :)
Nick Tomlin
4
warum nicht einfach if(index === array.length - 1)und entfernenitemsProcessed
Amin Jafari
5
@AminJafari, da die asynchronen Anrufe möglicherweise nicht in der genauen Reihenfolge aufgelöst werden, in der sie registriert sind (sagen wir, Sie rufen einen Server an und er bleibt beim zweiten Anruf leicht stehen, verarbeitet aber den letzten Anruf einwandfrei). Der letzte asynchrone Aufruf wurde möglicherweise vor den vorherigen aufgelöst. Das Mutieren eines Konters schützt davor, da alle Rückrufe unabhängig von der Reihenfolge, in der sie aufgelöst werden, ausgelöst werden müssen.
Nick Tomlin
25

Wenn Sie auf asynchrone Funktionen stoßen und sicherstellen möchten, dass der Code vor der Ausführung seiner Aufgabe beendet wird, können wir jederzeit die Rückruffunktion verwenden.

Beispielsweise:

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

Hinweis: functionAfterForEachIst die Funktion, die ausgeführt werden soll, nachdem alle Aufgaben abgeschlossen sind. asynchronousist die asynchrone Funktion, die in foreach ausgeführt wird.

Emil Reña Enriquez
quelle
9
Dies funktioniert nicht, da die Reihenfolge der Ausführung asynchroner Anforderungen nicht berücksichtigt wird. Die letzte asynchrone Anforderung könnte vor den anderen enden und functionAfterForEach () ausführen, bevor alle Anforderungen ausgeführt werden.
Rémy DAVID
@ RémyDAVID yep Sie haben einen Punkt in Bezug auf die Reihenfolge der Ausführung oder soll ich sagen, wie lange der Prozess jedoch abgeschlossen ist, Javascript ist Single-Threaded, so dass dies schließlich funktioniert. Und der Beweis ist die Gegenstimme, die diese Antwort erhalten hat.
Emil Reña Enriquez
1
Ich bin mir nicht sicher, warum Sie so viele positive Stimmen haben, aber Rémi hat Recht. Ihr Code funktioniert überhaupt nicht, da asynchron bedeutet, dass eine der Anforderungen jederzeit zurückgegeben werden kann. Obwohl JavaScript kein Multithread ist, ist es Ihr Browser. Schwer, könnte ich hinzufügen. Es kann daher jederzeit einen Ihrer Rückrufe in beliebiger Reihenfolge aufrufen, je nachdem, wann eine Antwort von einem Server
Alexis Wilke
2
Ja, diese Antwort ist völlig falsch. Wenn ich 10 Downloads parallel durchführe, ist fast garantiert, dass der letzte Download vor dem Rest abgeschlossen ist und somit die Ausführung beendet.
Knrdk
Ich würde vorschlagen, dass Sie einen Zähler verwenden, um die Anzahl der abgeschlossenen asynchronen Aufgaben zu erhöhen und diesen mit der Länge des Arrays anstelle des Index abzugleichen. Die Anzahl der Upvotes hat nichts mit dem Nachweis der Richtigkeit der Antwort zu tun.
Alex
17

Hoffe, dies wird Ihr Problem beheben, ich arbeite normalerweise damit, wenn ich forEach mit asynchronen Aufgaben ausführen muss.

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
      } 
}

mit

function doAsynchronousFunction(entry,callback){
       //asynchronousjob with entry
       callback();
}
Adnene Belfodil
quelle
Ich hatte ein ähnliches Problem in meinem Angular 9-Code und diese Antwort hat den Trick für mich getan. Obwohl die Antwort von @Emil Reña Enriquez auch für mich funktioniert hat, finde ich, dass dies eine genauere und einfachere Antwort auf dieses Problem ist.
Omostan
17

Es ist seltsam, wie viele falsche Antworten auf asynchrone Fälle gegeben wurden! Es kann einfach gezeigt werden, dass das Überprüfen des Index nicht das erwartete Verhalten liefert:

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

Ausgabe:

4000 started
2000 started
1: 2000
0: 4000

Wenn wir nach prüfen index === array.length - 1, wird der Rückruf nach Abschluss der ersten Iteration aufgerufen, während das erste Element noch aussteht!

Um dieses Problem zu lösen, ohne externe Bibliotheken wie Async zu verwenden, ist es meiner Meinung nach am besten, die Länge der Liste zu speichern und zu dekrementieren, wenn nach jeder Iteration. Da es nur einen Thread gibt, sind wir sicher, dass es keine Chance auf Rennbedingungen gibt.

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);
});
Rsh
quelle
1
Das ist wahrscheinlich die einzige Lösung. Verwendet die asynchrone Bibliothek auch Zähler?
Vanuan
1
Obwohl andere Lösungen die Aufgabe übernehmen, ist dies am überzeugendsten, da keine Verkettung oder zusätzliche Komplexität erforderlich ist. KISS
Azatar
Bitte berücksichtigen Sie auch die Situation, in der die Array-Länge Null ist. In diesem Fall wird der Rückruf niemals aufgerufen
Saeed Ir
6

Mit ES2018 können Sie asynchrone Iteratoren verwenden:

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');
}
Krzysztof Grzybek
quelle
1
Verfügbar in Node v10
Matt Swezey
2

Meine Lösung ohne Versprechen (dies stellt sicher, dass jede Aktion beendet wird, bevor die nächste beginnt):

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>

Jackstrapp
quelle
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();
 });
Hardik Shimpi
quelle
1
Es wird nicht funktionieren, denn wenn Sie in foreach einen asynchronen Vorgang ausführen.
Sudhanshu Gaur
0

Meine Lösung:

//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();
            }
        });
    }
});

Beispiel:

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

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

// out: a b c done
Gabor
quelle
Die Lösung ist innovativ, aber es kommt ein Fehler - "Aufgabe ist keine Funktion"
Genius
0

Ich versuche Easy Way, um es zu lösen, teile es mit dir:

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();
                    }
                });

requestist die Funktion der mssql-Bibliothek in Node js. Dies kann jede gewünschte Funktion oder jeden gewünschten Code ersetzen. Viel Glück

HamidReza Heydari
quelle
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)
})
Nilesh Pawar
quelle
-2

Sie sollten keinen Rückruf benötigen, um eine Liste zu durchlaufen. Fügen Sie einfach den end()Anruf nach der Schleife hinzu.

posts.forEach(function(v, i){
   res.write(v + ". Index " + i);
});
res.end();
azz
quelle
3
Nein. Das OP betonte, dass für jede Iteration eine asynchrone Logik ausgeführt wird. res.writeist KEINE asynchrone Operation, daher funktioniert Ihr Code nicht.
Jim G.
-2

Eine einfache Lösung wäre wie folgt

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

["a", "b", "c"].forEach(function(item, index, array){
    //code here
    if(i == array.length -1)
    callback()
}
molham556
quelle
3
Funktioniert nicht für asynchronen Code, der die gesamte Prämisse der Frage darstellt.
grg
-3

Wie wäre es mit setInterval, um die vollständige Iterationszahl zu überprüfen, bringt Garantie. Ich bin mir nicht sicher, ob es den Bereich nicht überlasten wird, aber ich benutze es und scheint es zu sein

_.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);
Tino Costa 'El Nino'
quelle
Das sieht logisch einfach aus
Zeal Murapa