Richiamata dopo aver completato tutte le richiamate asincrone per Ogni richiamata


245

Come suggerisce il titolo. Come faccio a fare questo?

Voglio chiamare whenAllDone()dopo che il ciclo forEach ha attraversato ogni elemento e ha eseguito un'elaborazione asincrona.

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

Possibile farlo funzionare in questo modo? Quando il secondo argomento di forEach è una funzione di callback che viene eseguita una volta che ha attraversato tutte le iterazioni?

Uscita prevista:

3 done
1 done
2 done
All done!

13
Sarebbe bello se il forEachmetodo array standard avesse doneparametri di allDonecallback e callback!
Vanuan,

22
È un vero peccato che qualcosa di così semplice richieda così tanto wrestling in JavaScript.
Ali,

Risposte:


410

Array.forEach non fornisce questa gentilezza (oh se lo fosse) ma ci sono diversi modi per realizzare ciò che vuoi:

Utilizzando un semplice contatore

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

var itemsProcessed = 0;

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

(grazie a @vanuan e altri) Questo approccio garantisce che tutti gli articoli vengano elaborati prima di invocare il callback "completato". È necessario utilizzare un contatore che viene aggiornato nel callback. A seconda del valore del parametro indice non fornisce la stessa garanzia, poiché l'ordine di restituzione delle operazioni asincrone non è garantito.

Utilizzo delle promesse ES6

(una libreria promessa può essere utilizzata per browser meno recenti):

  1. Elaborare tutte le richieste garantendo l'esecuzione sincrona (ad es. 1, quindi 2 e 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. Elabora tutte le richieste asincrone senza esecuzione "sincrona" (2 possono terminare più velocemente di 1)

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

Utilizzando una libreria asincrona

Esistono altre librerie asincrone, la più popolare è Async , che fornisce meccanismi per esprimere ciò che desideri.

modificare

Il corpo della domanda è stato modificato per rimuovere il codice di esempio precedentemente sincrono, quindi ho aggiornato la mia risposta per chiarire. L'esempio originale utilizzava un codice simile sincrono per modellare il comportamento asincrono, quindi si applicava quanto segue:

array.forEachè sincrono e così è res.write, quindi puoi semplicemente mettere il callback dopo la tua chiamata per foreach:

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

  res.end();

31
Si noti, tuttavia, che se all'interno di forEach sono presenti elementi asincroni (ad esempio, si esegue il looping di una matrice di URL e si esegue un HTTP GET su di essi), non esiste alcuna garanzia che res.end venga chiamato per ultimo.
AlexMA,

Per attivare un callback dopo che un'azione asincrona viene eseguita in un ciclo, è possibile utilizzare ciascun metodo dell'utilità asincrona: github.com/caolan/async#each
elkelk

2
@Vanuan ho aggiornato la mia risposta per abbinare meglio la tua modifica piuttosto significativa :)
Nick Tomlin il

4
perché non solo if(index === array.length - 1)e rimuovereitemsProcessed
Amin Jafari,

5
@AminJafari perché le chiamate asincrone potrebbero non risolversi nell'ordine esatto in cui sono registrate (supponiamo che tu stia chiamando un server e si blocchi leggermente alla seconda chiamata ma elabora l'ultima chiamata in modo corretto). L'ultima chiamata asincrona potrebbe essere risolta prima di quelle precedenti. La mutazione di un counter guard contro questo, poiché tutti i callback devono essere attivati ​​indipendentemente dall'ordine in cui vengono risolti.
Nick Tomlin,

25

Se si incontrano funzioni asincrone e si desidera assicurarsi che prima di eseguire il codice termina la sua attività, è sempre possibile utilizzare la funzionalità di callback.

Per esempio:

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

Nota: functionAfterForEachè la funzione da eseguire al termine di tutte le attività. asynchronousè la funzione asincrona eseguita all'interno di foreach.


9
Questo non funzionerà poiché l'ordine di esecuzione delle richieste asincrone non è protetto. L'ultima richiesta asincrona potrebbe terminare prima delle altre ed eseguire functionAfterForEach () prima che tutte le richieste vengano eseguite.
Rémy DAVID,

@ RémyDAVID sì, hai un punto sull'ordine di esecuzione o devo dire per quanto tempo è terminato il processo, essendo javascript a thread singolo, quindi alla fine funziona. E la prova è il voto positivo ricevuto da questa risposta.
Emil Reña Enriquez,

1
Non sono troppo sicuro del motivo per cui hai tanti voti positivi, ma Rémi ha ragione. Il tuo codice non funzionerà affatto poiché asincrono significa che qualsiasi richiesta può essere restituita in qualsiasi momento. Sebbene JavaScript non sia multithread, lo è il tuo browser. Pesantemente, potrei aggiungere. Può quindi chiamare uno qualsiasi dei tuoi callback in qualsiasi momento in qualsiasi ordine a seconda di quando viene ricevuta una risposta da un server ...
Alexis Wilke

2
sì, questa è la risposta è completamente sbagliata. Se eseguo 10 download in parallelo, è quasi certo che l'ultimo download termina in anticipo rispetto agli altri e quindi termina l'esecuzione.
Knrdk,

Suggerirei di utilizzare un contatore per aumentare il numero di attività asincrone completate e confrontarlo con la lunghezza dell'array anziché l'indice. Il numero di voti non ha nulla a che fare con la prova della correttezza della risposta.
Alex

17

Spero che questo risolva il tuo problema, di solito lavoro con questo quando devo eseguire per ogni operazione asincrona all'interno.

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

Avevo un problema simile nel mio codice Angular 9 e questa risposta mi ha aiutato. Anche se la risposta di @Emil Reña Enriquez ha funzionato anche per me, trovo che questa sia una risposta più precisa e semplice per questo problema.
omostan,

17

È strano quante risposte errate siano state date al caso asincrono ! Si può semplicemente dimostrare che il controllo dell'indice non fornisce il comportamento previsto:

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

produzione:

4000 started
2000 started
1: 2000
0: 4000

Se controlliamo index === array.length - 1, il callback verrà chiamato al completamento della prima iterazione, mentre il primo elemento è ancora in sospeso!

Per risolvere questo problema senza utilizzare librerie esterne come asincrono, penso che la soluzione migliore sia quella di salvare la lunghezza dell'elenco e diminuirla se dopo ogni iterazione. Dato che c'è solo un thread, siamo sicuri che non ci sono possibilità di condizioni di gara.

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
Questa è probabilmente l'unica soluzione. La libreria asincrona utilizza anche i contatori?
Vanuan,

1
Sebbene altre soluzioni facciano il lavoro, questo è molto interessante perché non richiede concatenamento o complessità aggiuntiva. BACIO
azatar

Si prega di considerare anche la situazione in cui la lunghezza dell'array è zero, in questo caso il callback non verrebbe mai chiamato
Saeed Ir

6

Con ES2018 è possibile utilizzare iteratori asincroni:

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
Availabe in Node v10
Matt Swezey,

2

La mia soluzione senza Promessa (questo assicura che ogni azione sia terminata prima che inizi quella successiva):

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
Non funzionerà perché se si avrà un'operazione asincrona all'interno di foreach.
Sudhanshu Gaur,


0

La mia soluzione:

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

Esempio:

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

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

// out: a b c done

La soluzione è innovativa ma sta arrivando un errore - "il compito non è una funzione"
Genius,

0

Provo Easy Way per risolverlo, condividerlo con te:

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

requestè la funzione della libreria mssql nel nodo js. Questo può sostituire ogni funzione o codice desiderato. In bocca al lupo


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

Non è necessario un callback per scorrere in un elenco. Basta aggiungere la end()chiamata dopo il loop.

posts.forEach(function(v, i){
   res.write(v + ". Index " + i);
});
res.end();

3
No. L'OP ha sottolineato che la logica asincrona verrebbe eseguita per ogni iterazione. res.writeNON è un'operazione asincrona, quindi il tuo codice non funzionerà.
Jim G.

-2

Una soluzione semplice sarebbe come seguire

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

["a", "b", "c"].forEach(function(item, index, array){
    //code here
    if(i == array.length -1)
    callback()
}

3
Non funziona per il codice asincrono, che è l'intera premessa della domanda.
gr

-3

Che ne dite di setInterval, per verificare il conteggio completo dell'iterazione, offre garanzia. non sono sicuro se non sovraccaricherà l'ambito, ma lo uso e sembra essere quello

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

Sembra logicamente semplice
Zeal Murapa,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.