Devolución de llamada después de completar todas las devoluciones de llamada asíncronas para cada

245

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

Quiero llamar whenAllDone()después de que forEach-loop haya pasado por cada elemento y haya realizado un 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 para 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
fuente
13
¡Sería bueno si el forEachmétodo de matriz estándar tuviera un doneparámetro de allDonedevolución de llamada y una devolución de llamada!
Vanuan
22
Es una verdadera lástima que algo tan simple requiera tanta lucha en JavaScript.
Ali

Respuestas:

410

Array.forEach no proporciona esta bondad (oh si lo fuera) pero hay varias formas 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 "hecho". Debe usar un contador que se actualice en la devolución de llamada. Dependiendo del valor del parámetro de índice no proporciona la misma garantía, porque el orden de retorno de las operaciones asincrónicas no está garantizado.

Usando promesas ES6

(una biblioteca prometedora se puede utilizar para navegadores antiguos):

  1. Procese todas las solicitudes que garanticen la ejecución síncrona (por ejemplo, 1, luego 2 y 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

Hay otras bibliotecas asincrónicas, asíncronas. es la más popular, que proporcionan mecanismos para expresar lo que desea.

Editar

El cuerpo de la pregunta se ha editado para eliminar el código de ejemplo sincrónico anterior, por lo que he actualizado mi respuesta para aclarar. El ejemplo original usaba código síncrono similar para modelar el comportamiento asíncrono, por lo que se aplica lo siguiente:

array.forEaches sincrónico y también lo es res.write, por lo que simplemente puede poner 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
fuente
31
Sin embargo, tenga en cuenta que si hay cosas asincrónicas dentro de forEach (por ejemplo, está recorriendo una matriz de URL y haciendo un HTTP GET en ellas), no hay garantía de que se llame al último.
AlexMA
Para activar una devolución de llamada después de que se realice una acción asincrónica en un bucle, puede usar cada método de la utilidad asincrónica: github.com/caolan/async#each
elkelk
2
@Vanuan, he actualizado mi respuesta para que coincida mejor con tu edición bastante significativa :)
Nick Tomlin
44
¿Por qué no solo if(index === array.length - 1)y eliminar?itemsProcessed
Amin Jafari
55
@AminJafari porque las llamadas asincrónicas pueden no resolverse en el orden exacto en que están registradas (digamos que está llamando a un servidor y se detiene un poco en la segunda llamada pero procesa la última llamada correctamente). La última llamada asincrónica podría resolverse antes que las anteriores. Mutar a un contador protege contra esto ya que todas las devoluciones de llamada deben activarse independientemente del orden en que se resuelvan.
Nick Tomlin
25

Si encuentra funciones asincrónicas y desea asegurarse de que antes de ejecutar el código finalice su tarea, siempre podemos utilizar 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á una vez finalizadas las tareas foreach. asynchronouses la función asincrónica ejecutada dentro de foreach.

Emil Reña Enriquez
fuente
99
Esto no funcionará ya que el orden de ejecución de las solicitudes asincrónicas no está guardado. La última solicitud asíncrona podría finalizar antes que las demás y ejecutar functionAfterForEach () antes de que se realicen todas las solicitudes.
Rémy DAVID
@ RémyDAVID sí, tiene un punto con respecto al orden de ejecución o debo decir cuánto tiempo ha finalizado el proceso, sin embargo, JavaScript tiene un solo subproceso para que esto funcione eventualmente. Y la prueba es el voto positivo que recibió esta respuesta.
Emil Reña Enríquez
1
No estoy muy seguro de por qué tienes tantos votos a favor, pero Rémi tiene razón. Su código no funcionará en absoluto, ya que asíncrono significa que cualquiera de las solicitudes puede devolverse en cualquier momento. Aunque JavaScript no es multiproceso, su navegador sí lo es. Fuertemente, podría agregar. Por lo tanto, puede llamar a cualquiera de sus devoluciones de llamada en cualquier momento en cualquier orden, dependiendo de cuándo se recibe una respuesta de un servidor ...
Alexis Wilke
2
Sí, esta respuesta es completamente incorrecta. Si ejecuto 10 descargas en paralelo, está casi garantizado que la última descarga finalice antes que el resto y, por lo tanto, finalice la ejecución.
knrdk
Te sugiero que uses un contador para aumentar el número de tareas asincrónicas completadas y compararlo con la longitud de la matriz en lugar del índice. El número de votos a favor no tiene nada que ver con la prueba de la exactitud de la respuesta.
Alex
17

Espero que esto solucione su problema, generalmente trabajo con esto cuando necesito ejecutar para cada uno con tareas asincrónicas dentro.

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
fuente
Estaba teniendo un problema similar en mi código Angular 9 y esta respuesta me funcionó. Aunque la respuesta de @Emil Reña Enriquez también funcionó para mí, creo que esta es una respuesta más precisa y simple para este problema.
omostan
17

¡Es extraño cuántas respuestas incorrectas se han dado al caso asincrónico ! Se puede mostrar 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);
});

salida:

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 que el primer elemento aún está pendiente!

Para resolver este problema sin usar bibliotecas externas como async, creo que su mejor opción es guardar 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 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
fuente
1
Esa es probablemente la única solución. ¿La biblioteca asíncrona también usa contadores?
Vanuan
1
Aunque otras soluciones hacen el trabajo, esto es más convincente porque no requiere encadenamiento o complejidad adicional. KISS
azatar
Tenga en cuenta también la situación cuando la longitud de la matriz es cero, en este caso, la devolución de llamada nunca se llamaría
Saeed Ir
6

Con ES2018 puede usar iteradores asíncronos:

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
fuente
1
Disponible en Nodo v10
Matt Swezey
2

Mi solución sin promesa (esto asegura que cada acción finalice antes de que comience la siguiente):

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
fuente
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
fuente
1
No funcionará porque si tendrá una operación asincrónica dentro de foreach.
Sudhanshu Gaur
0

Mi solución:

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

Ejemplo:

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

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

// out: a b c done
Gabor
fuente
La solución es innovadora pero se produce un error: "la tarea no es una función"
Genius
0

Intento con Easy Way para resolverlo, compártelo contigo:

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

requestes la función de la biblioteca mssql en el nodo js. Esto puede reemplazar cada función o código que desee. Buena suerte

HamidReza Heydari
fuente
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
fuente
-2

No debería necesitar una devolución de llamada para recorrer una lista. Simplemente agregue la end()llamada después del bucle.

posts.forEach(function(v, i){
   res.write(v + ". Index " + i);
});
res.end();
azz
fuente
3
No. El OP enfatizó que la lógica asincrónica se ejecutaría para cada iteración. res.writeNO es una operación asincrónica, por lo que su código no funcionará.
Jim G.
-2

Una solución simple sería como seguir

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

["a", "b", "c"].forEach(function(item, index, array){
    //code here
    if(i == array.length -1)
    callback()
}
molham556
fuente
3
No funciona para el código asincrónico, que es la premisa completa de la pregunta.
grg
-3

¿Qué tal setInterval, para verificar el recuento completo de iteraciones, trae garantía? no estoy seguro de si no sobrecargará el alcance, pero lo uso y parece ser el indicado

_.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 Niño'
fuente
Esto parece lógicamente simple
Zeal Murapa