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!

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

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.


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

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

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

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>


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

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


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

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

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

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

Cela semble logiquement simple
Zeal Murapa
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.