Rappel après tous les rappels asynchrones forEach sont terminés

245

Comme le titre l'indique. Comment puis-je faire cela?

Je veux appeler whenAllDone()après que la boucle forEach ait traversé chaque élément et effectué un traitement asynchrone.

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

Possible de le faire fonctionner comme ça? Lorsque le deuxième argument de forEach est une fonction de rappel qui s'exécute une fois qu'il a parcouru toutes les itérations?

Production attendue:

3 done
1 done
2 done
All done!
Dan Andreasson
la source
13
Ce serait bien si la forEachméthode de tableau standard avait un doneparamètre de allDonerappel et un rappel!
Vanuan
22
C'est vraiment dommage que quelque chose d'aussi simple nécessite autant de lutte en JavaScript.
Ali

Réponses:

410

Array.forEach ne fournit pas cette finesse (oh si c'est le cas) mais il existe plusieurs façons d'accomplir ce que vous voulez:

Utiliser un simple compteur

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

var itemsProcessed = 0;

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

(merci à @vanuan et à d'autres) Cette approche garantit que tous les éléments sont traités avant d'appeler le rappel "terminé". Vous devez utiliser un compteur qui est mis à jour dans le rappel. Selon la valeur du paramètre d'index ne fournit pas la même garantie, car l'ordre de retour des opérations asynchrones n'est pas garanti.

Utilisation des promesses ES6

(une bibliothèque de promesses peut être utilisée pour les anciens navigateurs):

  1. Traiter toutes les demandes garantissant une exécution synchrone (par exemple 1 puis 2 puis 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. Traiter toutes les demandes asynchrones sans exécution "synchrone" (2 peuvent se terminer plus rapidement que 1)

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

Utilisation d'une bibliothèque asynchrone

Il existe d'autres bibliothèques asynchrones, async étant la plus populaire, qui fournissent des mécanismes pour exprimer ce que vous voulez.

Éditer

Le corps de la question a été modifié pour supprimer l'exemple de code précédemment synchrone, j'ai donc mis à jour ma réponse pour clarifier. L'exemple d'origine utilisait du code synchrone pour modéliser le comportement asynchrone, donc les éléments suivants s'appliquaient:

array.forEachest synchrone et est res.writedonc, vous pouvez donc simplement mettre votre rappel après votre appel à foreach:

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

  res.end();
Nick Tomlin
la source
31
Notez cependant que s'il y a des éléments asynchrones à l'intérieur de forEach (par exemple, vous parcourez un tableau d'URL et effectuez un HTTP GET sur elles), il n'y a aucune garantie que res.end sera appelé en dernier.
AlexMA
Afin de déclencher un rappel après qu'une action asynchrone soit effectuée dans une boucle, vous pouvez utiliser chaque méthode de l'utilitaire asynchrone: github.com/caolan/async#each
elkelk
2
@Vanuan j'ai mis à jour ma réponse pour mieux correspondre à votre montage plutôt significatif :)
Nick Tomlin
4
pourquoi pas juste if(index === array.length - 1)et retireritemsProcessed
Amin Jafari
5
@AminJafari car les appels asynchrones peuvent ne pas être résolus dans l'ordre exact dans lequel ils sont enregistrés (par exemple, vous appelez vers un serveur et il se bloque légèrement lors du 2ème appel mais traite le dernier appel fin). Le dernier appel asynchrone pourrait être résolu avant les précédents. La mutation d'un compteur protège contre cela car tous les rappels doivent se déclencher quel que soit l'ordre dans lequel ils se résolvent.
Nick Tomlin
25

Si vous rencontrez des fonctions asynchrones et que vous voulez vous assurer qu'avant d'exécuter le code, il termine sa tâche, nous pouvons toujours utiliser la fonction de rappel.

Par exemple:

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

Remarque: functionAfterForEachest la fonction à exécuter une fois les tâches foreach terminées. asynchronousest la fonction asynchrone exécutée à l'intérieur de foreach.

Emil Reña Enriquez
la source
9
Cela ne fonctionnera pas car l'ordre d'exécution des requêtes asynchrones n'est pas prévenu. La dernière demande asynchrone pourrait se terminer avant les autres et exécuter functionAfterForEach () avant que toutes les demandes soient effectuées.
Rémy DAVID
@ RémyDAVID yep vous avez un point concernant l'ordre d'exécution ou dois-je dire combien de temps le processus est terminé cependant, le javascript étant monothread donc cela fonctionne finalement. Et la preuve est le vote positif que cette réponse a reçu.
Emil Reña Enriquez
1
Je ne sais pas trop pourquoi vous avez autant de votes positifs, mais Rémi a raison. Votre code ne fonctionnera pas du tout, car asynchrone signifie que toute demande peut revenir à tout moment. Bien que JavaScript ne soit pas multithreads, votre navigateur l'est. Lourdement, je pourrais ajouter. Il peut ainsi appeler n'importe lequel de vos rappels à tout moment dans n'importe quel ordre selon le moment où une réponse est reçue d'un serveur ...
Alexis Wilke
2
oui, cette réponse est complètement fausse. Si je lance 10 téléchargements en parallèle, il est presque garanti que le dernier téléchargement se termine avant les autres et met ainsi fin à l'exécution.
knrdk
Je suggère que vous utilisiez un compteur pour incrémenter le nombre de tâches asynchrones terminées et le faire correspondre à la longueur du tableau au lieu de l'index. Le nombre de votes positifs n'a rien à voir avec la preuve de l'exactitude de la réponse.
Alex
17

J'espère que cela résoudra votre problème, je travaille généralement avec cela lorsque j'ai besoin d'exécuter forEach avec des tâches asynchrones à l'intérieur.

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

avec

function doAsynchronousFunction(entry,callback){
       //asynchronousjob with entry
       callback();
}
Adnene Belfodil
la source
J'avais un problème similaire dans mon code Angular 9 et cette réponse a fait l'affaire pour moi. Bien que la réponse @Emil Reña Enriquez ait également fonctionné pour moi, mais je trouve que c'est une réponse plus précise et plus simple à ce problème.
omostan
17

C'est étrange combien de réponses incorrectes ont été données au cas asynchrone ! On peut simplement montrer que la vérification de l'index ne fournit pas le comportement attendu:

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

production:

4000 started
2000 started
1: 2000
0: 4000

Si nous vérifions index === array.length - 1, le rappel sera appelé à la fin de la première itération, tandis que le premier élément est toujours en attente!

Pour résoudre ce problème sans utiliser de bibliothèques externes telles que async, je pense que votre meilleur pari est d'enregistrer la longueur de la liste et de la décrémenter si après chaque itération. Puisqu'il n'y a qu'un fil, nous sommes sûrs qu'il n'y a aucune chance de condition de course.

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
la source
1
C'est probablement la seule solution. La bibliothèque asynchrone utilise-t-elle également des compteurs?
Vanuan
1
Bien que d'autres solutions fassent le travail, c'est plus convaincant car cela ne nécessite pas de chaînage ou de complexité supplémentaire. KISS
azatar
Veuillez également considérer la situation où la longueur du tableau est nulle, dans ce cas, le rappel ne sera jamais appelé
Saeed Ir
6

Avec ES2018, vous pouvez utiliser des itérateurs asynchrones:

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
la source
1
Disponible dans Node v10
Matt Swezey
2

Ma solution sans promesse (cela garantit que chaque action est terminée avant que la prochaine ne commence):

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
la source
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
la source
1
Cela ne fonctionnera pas car si vous aurez une opération asynchrone à l'intérieur de foreach.
Sudhanshu Gaur du
0

Ma solution:

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

Exemple:

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

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

// out: a b c done
Gabor
la source
La solution est innovante mais une erreur arrive - "la tâche n'est pas une fonction"
Genius
0

J'essaye Easy Way pour le résoudre, je le partage avec vous:

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

requestest la fonction de la bibliothèque mssql dans Node js. Cela peut remplacer chaque fonction ou code que vous souhaitez. Bonne chance

HamidReza Heydari
la source
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
la source
-2

Vous ne devriez pas avoir besoin d'un rappel pour parcourir une liste. Ajoutez simplement l' end()appel après la boucle.

posts.forEach(function(v, i){
   res.write(v + ". Index " + i);
});
res.end();
azz
la source
3
Non. L'OP a souligné que la logique asynchrone s'exécuterait pour chaque itération. res.writen'est PAS une opération asynchrone, donc votre code ne fonctionnera pas.
Jim G.
-2

Une solution simple serait comme suivre

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

["a", "b", "c"].forEach(function(item, index, array){
    //code here
    if(i == array.length -1)
    callback()
}
molham556
la source
3
Ne fonctionne pas pour le code asynchrone qui est la prémisse entière de la question.
grg
-3

Que diriez-vous de setInterval, pour vérifier le nombre d'itérations complet, apporte une garantie. Je ne sais pas si cela ne surchargera pas la portée, mais je l'utilise et semble être celui

_.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»
la source
Cela semble logiquement simple
Zeal Murapa