Comment puis-je attendre un ensemble de fonctions de rappel asynchrones?

95

J'ai du code qui ressemble à ceci en javascript:

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

Une fois TOUS ces appels asynchrones terminés, je veux calculer le min sur tous les tableaux.

Comment puis-je les attendre tous?

Ma seule idée pour le moment est d'avoir un tableau de booléens appelé done, et de définir done [i] sur true dans la ième fonction de rappel, puis de dire while (tout n'est pas fait) {}

edit: Je suppose qu'une solution possible, mais laide, serait d'éditer le tableau done dans chaque rappel, puis d'appeler une méthode si toutes les autres sont définies à partir de chaque rappel, ainsi le dernier rappel à terminer appellera la méthode continue.

Merci d'avance.

codeurs
la source
1
Sur async, voulez-vous dire attendre qu'une requête Ajax se termine?
Peter Aron Zentai
6
Remarque, while (not all are done) { }ne fonctionnerait pas. Pendant que vous attendez occupé, aucun de vos rappels ne peut s'exécuter.
cHao
Oui. J'attends un appel asynchrone à une API externe pour revenir afin qu'il déclenche les méthodes de rappel. Ouais cHao, j'ai réalisé ça, c'est pourquoi je demande de l'aide ici: D
codersarepeople
Vous pouvez essayer ceci: github.com/caolan/async Très bel ensemble de fonctions utilitaires asynchrones.
Paul Greyson

Réponses:

191

Vous n'avez pas été très précis avec votre code, alors je vais inventer un scénario. Supposons que vous ayez 10 appels ajax et que vous vouliez accumuler les résultats de ces 10 appels ajax, puis quand ils ont tous terminé, vous voulez faire quelque chose. Vous pouvez le faire comme ceci en accumulant les données dans un tableau et en gardant une trace de la fin du dernier:

Compteur manuel

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

Remarque: la gestion des erreurs est importante ici (non affichée car elle est spécifique à la façon dont vous effectuez vos appels ajax). Vous voudrez peut-être réfléchir à la manière dont vous allez gérer le cas où un appel ajax ne se termine jamais, soit avec une erreur, soit bloqué pendant une longue période ou expire après un long moment.


Promesses jQuery

Ajout à ma réponse en 2014. Ces jours-ci, les promesses sont souvent utilisées pour résoudre ce type de problème puisque jQuery's $.ajax()retourne déjà une promesse et vous $.when()fera savoir quand un groupe de promesses sont toutes résolues et collectera les résultats de retour pour vous:

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

Promesses standard ES6

Comme spécifié dans la réponse de kba : si vous avez un environnement avec des promesses natives intégrées (navigateur moderne ou node.js ou utilisant babeljs transpile ou utilisant un promesse polyfill), vous pouvez utiliser les promesses spécifiées par ES6. Consultez ce tableau pour la prise en charge du navigateur. Les promesses sont prises en charge dans à peu près tous les navigateurs actuels, à l'exception d'IE.

Si doAjax()retourne une promesse, vous pouvez le faire:

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 vous avez besoin de transformer une opération asynchrone sans promesse en une opération qui renvoie une promesse, vous pouvez la «promettre» comme ceci:

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

Et, puis utilisez le modèle ci-dessus:

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

Promesses Bluebird

Si vous utilisez une bibliothèque plus riche en fonctionnalités, telle que la bibliothèque de promesses Bluebird , elle possède des fonctions supplémentaires intégrées pour vous faciliter la tâche:

 var doAjax = Promise.promisify(someAsync);
 var someData = [...]
 Promise.map(someData, doAjax).then(function(results) {
     // all ajax results here
 }, function(err) {
     // some error here
 });
jfriend00
la source
4
@kba - Je n'aurais pas exactement appelé cette réponse obsolète car toutes les techniques sont toujours applicables, en particulier si vous utilisez déjà jQuery pour Ajax. Mais, je l'ai mis à jour de plusieurs manières pour inclure des promesses natives.
jfriend00
Ces jours-ci, il existe une solution beaucoup plus propre qui ne nécessite même pas de requête. Je le fais avec FetchAPI et Promises
philx_x
@philx_x - Que faites-vous du support IE et Safari?
jfriend00
@ jfriend00 github a créé un polyfill github.com/github/fetch . Ou je ne sais pas encore si Babel prend en charge Fetch. babeljs.io
philx_x
@philx_x - Je le pensais. Vous avez besoin d'une bibliothèque polyfill pour pouvoir utiliser fetch de nos jours. Cela prend un peu d'air dans votre commentaire sur le fait d'éviter une bibliothèque ajax. Fetch est bien, mais il reste des années avant de pouvoir l'utiliser sans polyfill. Ce n'est même pas encore dans la dernière version de tous les navigateurs. Fais, ça ne change vraiment rien à ma réponse. J'en avais un doAjax()qui retourne une promesse comme l'une des options. Même chose que fetch().
jfriend00
17

Arrivée à partir de 2015: nous avons maintenant des promesses natives dans le navigateur le plus récent (Edge 12, Firefox 40, Chrome 43, Safari 8, Opera 32 et navigateur Android 4.4.4 et iOS Safari 8.4, mais pas Internet Explorer, Opera Mini et les versions antérieures d'Android).

Si nous voulons effectuer 10 actions asynchrones et être notifié lorsqu'elles sont toutes terminées, nous pouvons utiliser le natif Promise.all, sans aucune bibliothèque externe:

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
la source
2
Promises.all()devrait être Promise.all().
jfriend00
1
Votre réponse doit également faire référence aux navigateurs que vous pouvez utiliser Promise.all()dans lesquels aucune version actuelle d'IE.
jfriend00
10

Vous pouvez utiliser l' objet Deferred de jQuery avec la méthode 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
});
Paul
la source
7
La question n'a pas été balisée, jQueryce qui signifie généralement que l'OP ne voulait pas de réponse jQuery.
jfriend00
8
@ jfriend00 Je ne voulais pas réinventer la roue alors qu'elle était déjà créée dans jQuery
Paul
4
@Paul donc plutôt que de réinventer la roue, y compris 40kb de déchets pour faire quelque chose de simple (différé)
Raynos
2
Mais tout le monde ne peut pas ou ne veut pas utiliser jQuery et la coutume ici sur SO est que vous l'indiquez en étiquetant votre question avec jQuery ou non.
jfriend00
4
L'appel $ .when est cet exemple est incorrect. Pour attendre un tableau de différés / promesses, vous devez utiliser $ .when.apply ($, promises) .then (function () {/ * faire des trucs * /}).
danw
9

Vous pouvez l'émuler comme ceci:

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

puis chaque appel asynchrone fait ceci:

countDownLatch.count++;

tandis que dans chaque rappel asynchrone à la fin de la méthode, vous ajoutez cette ligne:

countDownLatch.check();

En d'autres termes, vous émulez une fonctionnalité de comptage à rebours.

Eugène Retunsky
la source
Dans 99% de tous les cas d'utilisation, une promesse est la voie à suivre, mais j'aime cette réponse car elle illustre une méthode pour gérer le code Async dans des situations où un polyfill Promise est plus grand que le JS qui l'utilise!
Sukima le
6

C'est la manière la plus soignée à mon avis.

Promise.all

FetchAPI

(pour une raison quelconque, Array.map ne fonctionne pas dans les fonctions .then pour moi. Mais vous pouvez utiliser un .forEach et [] .concat () ou quelque chose de similaire)

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
la source
1
Je pense que cela doit être return responses.map(response => { return response.json(); }), ou return responses.map(response => response.json()).
1

Utilisez une bibliothèque de flux de contrôle comme 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
la source