Come posso attendere l'insieme di funzioni di callback asincrone?


95

Ho un codice simile a questo in javascript:

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

Dopo che TUTTE quelle chiamate asincrone sono state fatte, voglio calcolare il minimo su tutti gli array.

Come posso aspettare per tutti loro?

La mia unica idea in questo momento è di avere un array di booleani chiamato done, e impostare done [i] su true nella ith funzione di callback, quindi dire while (non tutti sono finiti) {}

modifica: suppongo che una possibile, ma brutta soluzione, sarebbe quella di modificare l'array done in ogni callback, quindi chiamare un metodo se tutti gli altri done sono impostati da ogni callback, quindi l'ultimo callback da completare chiamerà il metodo continue.

Grazie in anticipo.


1
Per asincrono, intendi aspettare il completamento di una richiesta Ajax?
Peter Aron Zentai

6
Nota, while (not all are done) { }non funzionerebbe. Mentre sei impegnato in attesa, nessuno dei tuoi callback può essere eseguito.
cHao

Sì. Attendo che venga restituita una chiamata asincrona a un'API esterna in modo che attivi i metodi di callback. Sì cHao, l'ho capito, ecco perché sto chiedendo aiuto qui: D
codersarepeople

Puoi provare questo: github.com/caolan/async Set molto carino di funzioni di utilità asincrone.
Paul Greyson

Risposte:


191

Non sei stato molto specifico con il tuo codice, quindi creerò uno scenario. Diciamo che hai 10 chiamate ajax e vuoi accumulare i risultati di quelle 10 chiamate ajax e poi quando hanno tutte completato vuoi fare qualcosa. Puoi farlo in questo modo accumulando i dati in un array e tenendo traccia di quando l'ultimo è finito:

Contatore manuale

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: la gestione degli errori è importante qui (non mostrata perché è specifica per come stai effettuando le chiamate ajax). Dovrai pensare a come gestirai il caso quando una chiamata ajax non viene mai completata, con un errore o si blocca per molto tempo o scade dopo molto tempo.


jQuery Promises

In aggiunta alla mia risposta nel 2014. In questi giorni, le promesse vengono spesso utilizzate per risolvere questo tipo di problema poiché jQuery $.ajax()restituisce già una promessa e $.when()ti farà sapere quando un gruppo di promesse sarà stato risolto e raccoglierà i risultati di ritorno per te:

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

Promesse standard ES6

Come specificato nella risposta di kba : se hai un ambiente con promesse native integrate (browser moderno o node.js o utilizzando babeljs transpile o utilizzando un polyfill di promesse), puoi utilizzare promesse specificate da ES6. Vedi questa tabella per il supporto del browser. Le promesse sono supportate praticamente in tutti i browser attuali, tranne IE.

Se doAjax()restituisce una promessa, puoi farlo:

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 devi trasformare un'operazione asincrona senza promessa in un'operazione che restituisce una promessa, puoi "prometterla" in questo modo:

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

E poi usa lo schema sopra:

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

Promesse di Bluebird

Se utilizzi una libreria più ricca di funzionalità come la libreria di promesse di Bluebird , ha alcune funzioni aggiuntive integrate per renderlo più semplice:

 var doAjax = Promise.promisify(someAsync);
 var someData = [...]
 Promise.map(someData, doAjax).then(function(results) {
     // all ajax results here
 }, function(err) {
     // some error here
 });

4
@kba - Non avrei definito esattamente questa risposta obsoleta poiché tutte le tecniche sono ancora applicabili, in particolare se stai già utilizzando jQuery per Ajax. Ma l'ho aggiornato in diversi modi per includere le promesse native.
jfriend00

in questi giorni c'è una soluzione molto più pulita che non ha nemmeno bisogno di jquery. Lo sto facendo con FetchAPI e Promises
philx_x

@philx_x - Cosa stai facendo per il supporto di IE e Safari?
jfriend00

@ jfriend00 github ha creato un polyfill github.com/github/fetch . Oppure non sono ancora sicuro che Babel supporti il ​​recupero. babeljs.io
philx_x

@philx_x - Lo pensavo. Hai bisogno di una libreria polyfill per usare fetch al giorno d'oggi. Prende un po 'd'aria il tuo commento sull'evitare una libreria ajax. Fetch è carino, ma mancano anni per poterlo utilizzare senza polyfill. Non è ancora nemmeno nell'ultima versione di tutti i browser. Sì, non cambia davvero nulla nella mia risposta. Avevo una doAjax()che restituisce una promessa come una delle opzioni. Stessa cosa di fetch().
jfriend00

17

Check-in dal 2015: ora abbiamo promesse native nel browser più recente (Edge 12, Firefox 40, Chrome 43, Safari 8, Opera 32 e browser Android 4.4.4 e iOS Safari 8.4, ma non Internet Explorer, Opera Mini e versioni precedenti di Android).

Se vogliamo eseguire 10 azioni asincrone e ricevere una notifica quando sono terminate, possiamo utilizzare il nativo Promise.all, senza alcuna libreria esterna:

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

2
Promises.all()dovrebbe essere Promise.all().
jfriend00

1
La tua risposta deve anche fare riferimento a quali browser puoi utilizzare Promise.all()in cui non include le versioni correnti di IE.
jfriend00

10

Puoi utilizzare l' oggetto Deferred di jQuery insieme al metodo 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
});

7
La domanda non è stata contrassegnata, il jQueryche di solito significa che l'OP non voleva una risposta jQuery.
jfriend00

8
@ jfriend00 Non volevo reinventare la ruota quando era già stata creata in jQuery
Paul

4
@Paul quindi piuttosto che reinventare la ruota includendo 40kb di spazzatura per fare qualcosa di semplice (differiti)
Raynos

2
Ma non tutti possono o vogliono usare jQuery e la consuetudine qui su SO è che tu lo indichi sia che tu tagghi la tua domanda con jQuery o meno.
jfriend00

4
La chiamata $ .quando è questo esempio non è corretta. Per attendere un array di promesse / differite devi usare $ .when.apply ($, promesse) .then (function () {/ * do stuff * /}).
danw

9

Puoi emularlo in questo modo:

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

quindi ogni chiamata asincrona fa questo:

countDownLatch.count++;

mentre in ogni richiamo asincrono alla fine del metodo aggiungi questa riga:

countDownLatch.check();

In altre parole, emuli una funzionalità di blocco del conto alla rovescia.


Nel 99% di tutti i casi d'uso una Promise è la strada da percorrere, ma mi piace questa risposta perché illustra un metodo per gestire il codice Async in situazioni in cui un polyfill Promise è più grande del JS che lo utilizza!
Sukima

6

Questo è il modo più pulito secondo me.

Prometti tutti

FetchAPI

(per qualche motivo Array.map non funziona all'interno delle funzioni .then per me. Ma puoi usare .forEach e [] .concat () o qualcosa di simile)

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

1
Penso che questo debba essere return responses.map(response => { return response.json(); }), o return responses.map(response => response.json()).

1

Usa una libreria del flusso di controllo come 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)
})
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.