Retorno de chamada após a conclusão de todos os retornos de chamada assíncronos forEach

245

Como o título sugere. Como eu faço isso?

Quero ligar whenAllDone()depois que o loop forEach passou por cada elemento e fez algum processamento assíncrono.

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

É possível fazê-lo funcionar assim? Quando o segundo argumento para forEach é uma função de retorno de chamada que é executada depois de todas as iterações?

Saída esperada:

3 done
1 done
2 done
All done!
Dan Andreasson
fonte
13
Seria bom se o forEachmétodo de matriz padrão tivesse doneparâmetro de allDoneretorno de chamada e retorno de chamada!
Vanuan
22
É uma pena que algo tão simples exija muita luta em JavaScript.
Ali

Respostas:

410

Array.forEach não fornece essa gentileza (se assim for), mas há várias maneiras de realizar o que você deseja:

Usando um contador simples

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

var itemsProcessed = 0;

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

(obrigado a @vanuan e outros) Essa abordagem garante que todos os itens sejam processados ​​antes de chamar o retorno de chamada "concluído". Você precisa usar um contador que seja atualizado no retorno de chamada. Dependendo do valor do parâmetro index, não fornece a mesma garantia, porque a ordem de retorno das operações assíncronas não é garantida.

Usando as promessas do ES6

(uma biblioteca de promessas pode ser usada para navegadores mais antigos):

  1. Processe todas as solicitações que garantam a execução síncrona (por exemplo, 1 e 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. Processe todas as solicitações assíncronas sem execução "síncrona" (2 podem terminar mais 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 uma biblioteca assíncrona

Existem outras bibliotecas assíncronas, async sendo as mais populares, que fornecem mecanismos para expressar o que você deseja.

Editar

O corpo da pergunta foi editado para remover o código de exemplo anteriormente síncrono. Atualizei minha resposta para esclarecer. O exemplo original usava o código síncrono para modelar o comportamento assíncrono; portanto, o seguinte se aplica:

array.forEaché síncrono e assim é res.write, para que você possa simplesmente colocar seu retorno de chamada após sua ligação para foreach:

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

  res.end();
Nick Tomlin
fonte
31
Observe, no entanto, que se houver coisas assíncronas dentro do forEach (por exemplo, você percorre uma matriz de URLs e executa um HTTP GET nelas), não há garantia de que res.end será chamado por último.
AlexMA #
A fim de disparar um retorno após uma ação assíncrona é executada em um loop que você pode usar o utilitário assíncrona é cada método: github.com/caolan/async#each
elkelk
2
@Vanuan Eu atualizei a minha resposta para melhor atender a sua edição bastante significativo :)
Nick Tomlin
4
porque não basta if(index === array.length - 1)e removaitemsProcessed
Amin Jafari
5
@AminJafari porque as chamadas assíncronas podem não ser resolvidas na ordem exata em que são registradas (digamos que você esteja ligando para um servidor e ele fica um pouco parado na segunda chamada, mas processa bem a última chamada). A última chamada assíncrona pode ser resolvida antes das anteriores. Mutar um contador protege contra isso, pois todos os retornos de chamada devem ser acionados, independentemente da ordem em que são resolvidos.
21416 Nick Atlin
25

Se você encontrar funções assíncronas e quiser ter certeza de que, antes de executar o código, ele conclui sua tarefa, sempre podemos usar o recurso de retorno de chamada.

Por exemplo:

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

Nota: functionAfterForEaché a função a ser executada após a conclusão das tarefas foreach. asynchronousé a função assíncrona executada dentro do foreach.

Emil Reña Enriquez
fonte
9
Isso não funcionará, pois a ordem de execução de solicitações assíncronas não é garantida. A última solicitação assíncrona pode terminar antes das outras e executar functionAfterForEach () antes que todas as solicitações sejam concluídas.
Rémy DAVID
@ RémyDAVID: sim, você tem um ponto em relação à ordem de execução ou devo dizer quanto tempo o processo está concluído, no entanto, o javascript é de thread único para que funcione eventualmente. E a prova é o voto positivo desta resposta recebida.
Emil Rena Enriquez
1
Não sei ao certo por que você tem tantos votos positivos, mas Rémi está correto. Seu código não funcionará, pois assíncrono significa que qualquer solicitação pode retornar a qualquer momento. Embora o JavaScript não seja multithreads, seu navegador é. Pesadamente, devo acrescentar. Assim, pode chamar qualquer um de seus retornos de chamada a qualquer momento em qualquer ordem dependendo de quando a resposta é recebida de um servidor ...
Alexis Wilke
2
Sim, esta é a resposta está completamente errada. Se eu executar 10 downloads em paralelo, é quase garantido que o último download termine à frente do restante e, assim, encerre a execução.
knrdk
Eu sugiro que você use um contador para aumentar o número de tarefas assíncronas concluídas e faça a correspondência com o comprimento da matriz em vez do índice. O número de votos positivos não tem nada a ver com a prova da exatidão da resposta.
Alex
17

Espero que isso resolva o seu problema, geralmente trabalho com isso quando preciso executar o forEach com tarefas assíncronas 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
      } 
}

com

function doAsynchronousFunction(entry,callback){
       //asynchronousjob with entry
       callback();
}
Adnene Belfodil
fonte
Eu estava tendo um problema semelhante no meu código Angular 9 e esta resposta foi o que me ocorreu. Embora a resposta de @Emil Reña Enriquez também tenha funcionado para mim, acho a resposta mais precisa e simples para esse problema.
omostan
17

É estranho quantas respostas incorretas foram dadas ao caso assíncrono ! Pode-se simplesmente mostrar que a verificação do índice não fornece o comportamento esperado:

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

resultado:

4000 started
2000 started
1: 2000
0: 4000

Se procurarmos index === array.length - 1, o retorno de chamada será chamado após a conclusão da primeira iteração, enquanto o primeiro elemento ainda estiver pendente!

Para resolver esse problema sem usar bibliotecas externas, como async, acho que sua melhor aposta é economizar o comprimento da lista e diminuir se após cada iteração. Como há apenas um tópico, temos certeza de que não há chance de condição de corrida.

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
fonte
1
Essa é provavelmente a única solução. A biblioteca assíncrona também usa contadores?
Vanuan
1
Embora outras soluções façam o trabalho, isso é mais atraente, porque não requer encadeamento ou complexidade adicional. KISS
azatar 06/09
Considere também a situação em que o comprimento do array é zero; nesse caso, o retorno de chamada nunca seria chamado
Saeed Ir
6

Com o ES2018, você pode usar iteradores assí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
fonte
1
Disponível no Node v10 #
Matt Swezey
2

Minha solução sem promessa (isso garante que todas as ações sejam encerradas antes do início da próxima):

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
fonte
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
fonte
1
Não funcionará porque se você tiver uma operação assíncrona no foreach.
Sudhanshu Gaur
0

Minha solução:

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

Exemplo:

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

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

// out: a b c done
Gabor
fonte
A solução é inovadora, mas está chegando um erro - "tarefa não é uma função"
Genius
0

Tento o Easy Way para resolvê-lo e compartilhá-lo com você:

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é a função da biblioteca mssql no nó js. Isso pode substituir cada função ou código que você deseja. Boa sorte

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

Você não precisa de um retorno de chamada para iterar através de uma lista. Basta adicionar a end()chamada após o loop.

posts.forEach(function(v, i){
   res.write(v + ". Index " + i);
});
res.end();
azz
fonte
3
Não. O OP enfatizou que a lógica assíncrona seria executada para cada iteração. res.writeNÃO é uma operação assíncrona, portanto seu código não funcionará.
Jim G.
-2

Uma solução simples seria 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
fonte
3
Não funciona para código assíncrono, que é a premissa inteira da pergunta.
grg
-3

Que tal setInterval, para verificar a contagem completa de iterações, traz garantia. Não tenho certeza se ele não sobrecarregará o escopo, mas eu o uso e parece ser o único

_.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'
fonte
Isso parece logicamente simples
Zeal Murapa