¿Cómo puedo esperar el conjunto de funciones de devolución de llamada asincrónicas?

95

Tengo un código que se parece a esto en javascript:

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

Una vez realizadas TODAS esas llamadas asíncronas, quiero calcular el mínimo de todas las matrices.

¿Cómo puedo esperar a todos?

Mi única idea en este momento es tener una matriz de valores booleanos llamados done, y establecer done [i] en verdadero en la función de devolución de llamada i-ésima, luego decir while (no todos están listos) {}

editar: Supongo que una solución posible, pero fea, sería editar la matriz done en cada devolución de llamada, luego llamar a un método si todos los demás hechos se establecen desde cada devolución de llamada, por lo que la última devolución de llamada para completar llamará al método continuo.

Gracias por adelantado.

codificadores
fuente
1
En async, ¿te refieres a esperar a que se complete una solicitud de Ajax?
Peter Aron Zentai
6
Tenga en cuenta while (not all are done) { }que no funcionaría. Mientras está ocupado esperando, ninguna de sus devoluciones de llamada puede ejecutarse.
cHao
Si. Estoy esperando una llamada asíncrona a una API externa para que regrese para que active los métodos de devolución de llamada. Sí, chao, me di cuenta de eso, por eso estoy pidiendo ayuda aquí: D
codersarepeople
Puede probar esto: github.com/caolan/async Muy buen conjunto de funciones de utilidad asíncrona.
Paul Greyson

Respuestas:

191

No has sido muy específico con tu código, así que inventaré un escenario. Digamos que tiene 10 llamadas ajax y desea acumular los resultados de esas 10 llamadas ajax y luego, cuando todas se hayan completado, desea hacer algo. Puede hacerlo así acumulando los datos en una matriz y haciendo un seguimiento de cuándo ha terminado el último:

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: el manejo de errores es importante aquí (no se muestra porque es específico de cómo está haciendo sus llamadas ajax). Querrá pensar en cómo manejará el caso cuando una llamada ajax nunca se complete, ya sea con un error o se atasque durante mucho tiempo o se agote después de mucho tiempo.


Promesas de jQuery

Agregando a mi respuesta en 2014. En estos días, las promesas se usan a menudo para resolver este tipo de problema, ya que jQuery $.ajax()ya devuelve una promesa y $.when()le informará cuando un grupo de promesas se resuelvan y recopilará los resultados devueltos por usted:

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

Promesas estándar de ES6

Como se especifica en la respuesta de kba : si tiene un entorno con promesas nativas integradas (navegador moderno o node.js o usando babeljs transpile o usando un polyfill de promesa), entonces puede usar promesas especificadas por ES6. Consulte esta tabla para conocer la compatibilidad con el navegador. Las promesas son compatibles con casi todos los navegadores actuales, excepto IE.

Si doAjax()devuelve una promesa, puede hacer esto:

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

Si necesita convertir una operación asíncrona sin promesa en una que devuelva una promesa, puede "promisificarla" de esta manera:

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

Y luego usa el patrón anterior:

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

Promesas de Bluebird

Si usa una biblioteca con más funciones, como la biblioteca de promesas Bluebird , entonces tiene algunas funciones adicionales integradas para facilitar esto:

 var doAjax = Promise.promisify(someAsync);
 var someData = [...]
 Promise.map(someData, doAjax).then(function(results) {
     // all ajax results here
 }, function(err) {
     // some error here
 });
jfriend00
fuente
4
@kba: no habría dicho exactamente que esta respuesta está desactualizada, ya que todas las técnicas aún son aplicables, especialmente si ya está usando jQuery para Ajax. Pero lo he actualizado de varias formas para incluir promesas nativas.
jfriend00
Estos días hay una solución mucho más limpia que ni siquiera necesita jquery. Lo estoy haciendo con FetchAPI y Promises
philx_x
@philx_x - ¿Qué está haciendo acerca de la compatibilidad con IE y Safari?
jfriend00
@ jfriend00 github hizo un polyfill github.com/github/fetch . O no estoy seguro de si babel es compatible con fetch todavía. babeljs.io
philx_x
@philx_x - Eso pensé. Necesita una biblioteca polyfill para poder usar fetch hoy en día. Toma un poco de aire su comentario sobre cómo evitar una biblioteca ajax. Fetch es bueno, pero faltan años para poder usarlo sin polyfill. Ni siquiera está en la última versión de todos los navegadores. Hazlo, realmente no cambia nada en mi respuesta. Tenía un doAjax()que devuelve una promesa como una de las opciones. Lo mismo que fetch().
jfriend00
17

Registro desde 2015: ahora tenemos promesas nativas en el navegador más reciente (Edge 12, Firefox 40, Chrome 43, Safari 8, Opera 32 y el navegador Android 4.4.4 y iOS Safari 8.4, pero no Internet Explorer, Opera Mini y versiones anteriores de Android).

Si queremos realizar 10 acciones asíncronas y recibir una notificación cuando todas hayan terminado, podemos usar la nativa Promise.all, sin bibliotecas externas:

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
fuente
2
Promises.all()debería ser Promise.all().
jfriend00
1
Su respuesta también debe referirse a los navegadores que puede utilizar Promise.all()en los que no se incluyen versiones actuales de IE.
jfriend00
10

Puede usar el objeto Deferred de jQuery junto con el 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
});
Pablo
fuente
7
La pregunta no estaba etiquetada, lo jQueryque generalmente significa que el OP no quería una respuesta de jQuery.
jfriend00
8
@ jfriend00 No quería reinventar la rueda cuando ya estaba creada en jQuery
Paul
4
@Paul, así que en lugar de reinventar la rueda, se incluyen 40 kb de basura para hacer algo simple (aplazados)
Raynos
2
Pero no todo el mundo puede o quiere usar jQuery y la costumbre aquí en SO es que usted indique si etiqueta su pregunta con jQuery o no.
jfriend00
4
El $ .when call es este ejemplo es incorrecto. Para esperar una matriz de promesas / diferidas, debe usar $ .when.apply ($, promises). Then (function () {/ * do stuff * /}).
danw
9

Puedes emularlo así:

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

luego cada llamada asíncrona hace esto:

countDownLatch.count++;

mientras que en cada llamada asynch al final del método, agrega esta línea:

countDownLatch.check();

En otras palabras, emula una funcionalidad de pestillo de cuenta regresiva.

Eugene Retunsky
fuente
En el 99% de todos los casos de uso, una Promise es el camino a seguir, pero me gusta esta respuesta porque ilustra un método para administrar el código Async en situaciones en las que un polyfill de Promise es más grande que el JS que lo usa.
Sukima
6

Esta es la forma más ordenada en mi opinión.

Promise.all

FetchAPI

(por alguna razón, Array.map no funciona dentro de .then funciona para mí. Pero puedes usar un .forEach y [] .concat () o algo similar)

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
fuente
1
Creo que esto debe ser return responses.map(response => { return response.json(); }), o return responses.map(response => response.json()).
1

Utilice una biblioteca de flujo de control 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
fuente