Como posso esperar pelo conjunto de funções de retorno de chamada assíncronas?

95

Tenho um código parecido com este em javascript:

forloop {
    //async call, returns an array to its callback
}

Depois que TODAS essas chamadas assíncronas forem feitas, quero calcular o mínimo de todas as matrizes.

Como posso esperar por todos eles?

Minha única ideia agora é ter um array de booleanos chamado done e definir done [i] como true na i-ésima função de retorno de chamada e dizer while (nem todos estão concluídos) {}

editar: Suponho que uma solução possível, mas feia, seria editar o array done em cada retorno de chamada e, em seguida, chamar um método se todos os outros feitos forem definidos em cada retorno de chamada, portanto, o último retorno de chamada a ser concluído chamará o método contínuo.

Desde já, obrigado.

codificadores são pessoas
fonte
1
No assíncrono, você quer dizer aguardar a conclusão de uma solicitação Ajax?
Peter Aron Zentai
6
Nota, while (not all are done) { }não funcionaria. Enquanto você está ocupado esperando, nenhum de seus retornos de chamada pode ser executado.
cHao
Sim. Estou aguardando o retorno de uma chamada assíncrona para uma API externa para que ele dispare os métodos de retorno de chamada. Sim cHao, eu percebi isso, é por isso que estou pedindo ajuda aqui: D
codificadores são pessoas
Você pode tentar isto: github.com/caolan/async Conjunto muito bom de funções de utilitário assíncrono.
Paul Greyson

Respostas:

191

Você não foi muito específico com seu código, então vou inventar um cenário. Digamos que você tenha 10 chamadas ajax e deseja acumular os resultados dessas 10 chamadas ajax e, quando todas elas forem concluídas, você deseja fazer algo. Você pode fazer isso assim, acumulando os dados em uma matriz e acompanhando quando o último foi concluído:

Contador Manual

var ajaxCallsRemaining = 10;
var returnedData = [];

for (var i = 0; i < 10; i++) {
    doAjax(whatever, function(response) {
        // success handler from the ajax call

        // save response
        returnedData.push(response);

        // see if we're done with the last ajax call
        --ajaxCallsRemaining;
        if (ajaxCallsRemaining <= 0) {
            // all data is here now
            // look through the returnedData and do whatever processing 
            // you want on it right here
        }
    });
}

Nota: o tratamento de erros é importante aqui (não mostrado porque é específico para como você está fazendo suas chamadas ajax). Você vai querer pensar sobre como vai lidar com o caso em que uma chamada de ajax nunca é concluída, seja com um erro ou travada por um longo tempo ou expira após um longo tempo.


jQuery Promises

Somando-se a minha resposta em 2014. Hoje em dia, as promessas costumam ser usadas para resolver esse tipo de problema, já que a jQuery's $.ajax()já retorna uma promessa e $.when()avisará quando um grupo de promessas for resolvido e coletará os resultados de retorno para você:

var promises = [];
for (var i = 0; i < 10; i++) {
    promises.push($.ajax(...));
}
$.when.apply($, promises).then(function() {
    // returned data is in arguments[0][0], arguments[1][0], ... arguments[9][0]
    // you can process it here
}, function() {
    // error occurred
});

ES6 Standard Promises

Conforme especificado na resposta de kba : se você tem um ambiente com promessas nativas integradas (navegador moderno ou node.js ou usando transpile babeljs ou usando um polyfill de promessa), então você pode usar promessas especificadas no ES6. Consulte esta tabela para suporte ao navegador. As promessas são suportadas em praticamente todos os navegadores atuais, exceto o IE.

Se doAjax()retornar uma promessa, você pode fazer o seguinte:

var promises = [];
for (var i = 0; i < 10; i++) {
    promises.push(doAjax(...));
}
Promise.all(promises).then(function() {
    // returned data is in arguments[0], arguments[1], ... arguments[n]
    // you can process it here
}, function(err) {
    // error occurred
});

Se você precisar transformar uma operação assíncrona sem promessa em uma que retorne uma promessa, poderá "prometê-la" assim:

function doAjax(...) {
    return new Promise(function(resolve, reject) {
        someAsyncOperation(..., function(err, result) {
            if (err) return reject(err);
            resolve(result);
        });
    });
}

E, em seguida, use o padrão acima:

var promises = [];
for (var i = 0; i < 10; i++) {
    promises.push(doAjax(...));
}
Promise.all(promises).then(function() {
    // returned data is in arguments[0], arguments[1], ... arguments[n]
    // you can process it here
}, function(err) {
    // error occurred
});

Bluebird Promises

Se você usar uma biblioteca com mais recursos, como a biblioteca de promessa do Bluebird , ela terá algumas funções adicionais integradas para tornar isso mais fácil:

 var doAjax = Promise.promisify(someAsync);
 var someData = [...]
 Promise.map(someData, doAjax).then(function(results) {
     // all ajax results here
 }, function(err) {
     // some error here
 });
jfriend00
fonte
4
@kba - Eu não diria exatamente esta resposta desatualizada, uma vez que todas as técnicas ainda são aplicáveis, principalmente se você já estiver usando jQuery para Ajax. Mas eu o atualizei de várias maneiras para incluir promessas nativas.
jfriend00
Hoje em dia existe uma solução muito mais limpa que nem precisa do jquery. Estou fazendo isso com FetchAPI e Promises
philx_x
@philx_x - O que você está fazendo em relação ao suporte do IE e Safari?
jfriend00
@ jfriend00 github fez um polyfill github.com/github/fetch . Ou não tenho certeza se o babel suporta fetch ainda. babeljs.io
philx_x
@philx_x - Pensei assim. Você precisa de uma biblioteca polyfill para usar fetch hoje em dia. Retira um pouco o seu comentário sobre como evitar uma biblioteca ajax. Fetch é bom, mas está a anos de ser capaz de usá-lo sem um polyfill. Ainda não está na versão mais recente de todos os navegadores. Faça, isso realmente não muda nada na minha resposta. Tive um doAjax()que retorna uma promessa como uma das opções. A mesma coisa que fetch().
jfriend00
17

Verificando a partir de 2015: agora temos promessas nativas no navegador mais recente (Edge 12, Firefox 40, Chrome 43, Safari 8, Opera 32 e navegador Android 4.4.4 e iOS Safari 8.4, mas não Internet Explorer, Opera Mini e versões anteriores do Android).

Se quisermos realizar 10 ações assíncronas e ser notificados quando todas terminarem, podemos usar o nativo Promise.all, sem nenhuma biblioteca externa:

function asyncAction(i) {
    return new Promise(function(resolve, reject) {
        var result = calculateResult();
        if (result.hasError()) {
            return reject(result.error);
        }
        return resolve(result);
    });
}

var promises = [];
for (var i=0; i < 10; i++) {
    promises.push(asyncAction(i));
}

Promise.all(promises).then(function AcceptHandler(results) {
    handleResults(results),
}, function ErrorHandler(error) {
    handleError(error);
});
kba
fonte
2
Promises.all()deveria ser Promise.all().
jfriend00
1
Sua resposta também precisa se referir a quais navegadores você pode usar Promise.all(), o que não inclui versões atuais do IE.
jfriend00
10

Você pode usar o objeto Deferred do jQuery junto com o método when .

deferredArray = [];
forloop {
    deferred = new $.Deferred();
    ajaxCall(function() {
      deferred.resolve();
    }
    deferredArray.push(deferred);
}

$.when(deferredArray, function() {
  //this code is called after all the ajax calls are done
});
Paulo
fonte
7
A pergunta não foi marcada, o jQueryque geralmente significa que o OP não queria uma resposta do jQuery.
jfriend00
8
@ jfriend00 Eu não queria reinventar a roda quando ela já estava criada no jQuery
Paul
4
@Paul então, em vez de reinventar a roda, incluindo 40kb de lixo para fazer algo simples (adiado)
Raynos
2
Mas nem todo mundo pode ou quer usar jQuery e o costume aqui no SO é que você indica isso marcando sua pergunta com jQuery ou não.
jfriend00
4
A chamada $ .when é este exemplo está incorreta. Para esperar por uma série de adiadas / promessas, você precisa usar $ .when.apply ($, promessas) .then (function () {/ * do stuff * /}).
danw
9

Você pode emular assim:

  countDownLatch = {
     count: 0,
     check: function() {
         this.count--;
         if (this.count == 0) this.calculate();
     },
     calculate: function() {...}
  };

então, cada chamada assíncrona faz isso:

countDownLatch.count++;

enquanto em cada chamada assíncrona no final do método, você adiciona esta linha:

countDownLatch.check();

Em outras palavras, você emula uma funcionalidade de trava de contagem regressiva.

Eugene Retunsky
fonte
Em 99% de todos os casos de uso, uma Promise é o caminho a seguir, mas eu gosto dessa resposta porque ela ilustra um método para gerenciar o código Async em situações onde um polyfill Promise é maior do que o JS que o usa!
Sukima
6

Essa é a maneira mais bacana na minha opinião.

Promise.all

FetchAPI

(por algum motivo, Array.map não funciona dentro das funções .então para mim. Mas você pode usar um .forEach e [] .concat () ou algo semelhante)

Promise.all([
  fetch('/user/4'),
  fetch('/user/5'),
  fetch('/user/6'),
  fetch('/user/7'),
  fetch('/user/8')
]).then(responses => {
  return responses.map(response => {response.json()})
}).then((values) => {
  console.log(values);
})
philx_x
fonte
1
Eu acho que isso precisa ser return responses.map(response => { return response.json(); }), ou return responses.map(response => response.json()).
1

Use uma biblioteca de fluxo de controle como after

after.map(array, function (value, done) {
    // do something async
    setTimeout(function () {
        // do something with the value
        done(null, value * 2)
    }, 10)
}, function (err, mappedArray) {
    // all done, continue here
    console.log(mappedArray)
})
Raynos
fonte