Devolución de llamada después de que se completen todas las devoluciones de llamada asincrónicas para cada uno

Resuelto Dan Andreasson asked hace 11 años • 13 respuestas

Como sugiere el título. ¿Cómo hago esto?

Quiero llamar whenAllDone()después de que el bucle forEach haya pasado por cada elemento y haya realizado algún procesamiento asincrónico.

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

¿Es posible hacer que funcione así? ¿Cuándo el segundo argumento de forEach es una función de devolución de llamada que se ejecuta una vez que pasó por todas las iteraciones?

Rendimiento esperado:

3 done
1 done
2 done
All done!
Dan Andreasson avatar Sep 24 '13 20:09 Dan Andreasson
Aceptado

Array.forEachno proporciona esta sutileza (oh, si lo fuera) pero hay varias maneras de lograr lo que desea:

Usando un contador simple

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

var itemsProcessed = 0;

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

(gracias a @vanuan y otros) Este enfoque garantiza que todos los elementos se procesen antes de invocar la devolución de llamada "hecha". Debe utilizar un contador que se actualice en la devolución de llamada. Dependiendo del valor del parámetro índice no ofrece la misma garantía, porque no se garantiza el orden de retorno de las operaciones asincrónicas.

Usando promesas de ES6

(Se puede utilizar una biblioteca de promesas para navegadores más antiguos):

  1. Procese todas las solicitudes garantizando la ejecución sincrónica (por ejemplo, 1, luego 2 y luego 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. Procese todas las solicitudes asíncronas sin ejecución "sincrónica" (2 pueden finalizar más rápido que 1)

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

Usando una biblioteca asíncrona

Existen otras bibliotecas asincrónicas, siendo async la más popular, que proporcionan mecanismos para expresar lo que desea.

Editar

El cuerpo de la pregunta se editó para eliminar el código de ejemplo previamente sincrónico, por lo que actualicé mi respuesta para aclarar. El ejemplo original utilizaba código similar a síncrono para modelar el comportamiento asincrónico, por lo que se aplicaba lo siguiente:

array.forEaches sincrónico y también lo es res.write, por lo que simplemente puede colocar su devolución de llamada después de su llamada a foreach:

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

  res.end();
Nick Tomlin avatar Sep 24 '2013 13:09 Nick Tomlin

Si encuentra funciones asincrónicas y desea asegurarse de que antes de ejecutar el código finalice su tarea, siempre podemos usar la capacidad de devolución de llamada.

Por ejemplo:

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

Nota: functionAfterForEaches la función que se ejecutará después de finalizar cada tarea. asynchronouses la función asincrónica ejecutada dentro de foreach.

Emil Reña Enriquez avatar Jun 03 '2014 10:06 Emil Reña Enriquez

Espero que esto solucione tu problema, normalmente trabajo con esto cuando necesito ejecutar forEach con tareas asincrónicas internas.

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

con

function doAsynchronousFunction(entry,callback){
       //asynchronousjob with entry
       callback();
}
Adnene Belfodil avatar Nov 12 '2015 14:11 Adnene Belfodil

¡Es extraño cuántas respuestas incorrectas se han dado en casos asincrónicos ! Se puede demostrar simplemente que la comprobación del índice no proporciona el comportamiento esperado:

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

producción:

4000 started
2000 started
1: 2000
0: 4000

Si verificamos index === array.length - 1, se llamará a la devolución de llamada al finalizar la primera iteración, ¡mientras el primer elemento aún esté pendiente!

Para resolver este problema sin utilizar bibliotecas externas como async, creo que lo mejor es ahorrar la longitud de la lista y disminuirla después de cada iteración. Dado que solo hay un hilo, estamos seguros de que no hay posibilidad de que se produzca una condición de carrera.

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 avatar Nov 15 '2015 10:11 Rsh