Passez un tableau de différés à $ .when ()

447

Voici un exemple artificiel de ce qui se passe: http://jsfiddle.net/adamjford/YNGcm/20/

HTML:

<a href="#">Click me!</a>
<div></div>

JavaScript:

function getSomeDeferredStuff() {
    var deferreds = [];

    var i = 1;
    for (i = 1; i <= 10; i++) {
        var count = i;

        deferreds.push(
        $.post('/echo/html/', {
            html: "<p>Task #" + count + " complete.",
            delay: count
        }).success(function(data) {
            $("div").append(data);
        }));
    }

    return deferreds;
}

$(function() {
    $("a").click(function() {
        var deferreds = getSomeDeferredStuff();

        $.when(deferreds).done(function() {
            $("div").append("<p>All done!</p>");
        });
    });
});

Je veux "Tout est fait!" à apparaître une fois toutes les tâches différées terminées, mais $.when()ne semble pas savoir comment gérer un tableau d'objets différés. "Terminé!" se produit d'abord parce que le tableau n'est pas un objet différé, donc jQuery va de l'avant et suppose que c'est juste fait.

Je sais que l'on pourrait passer les objets dans la fonction comme $.when(deferred1, deferred2, ..., deferredX)mais on ne sait pas combien d'objets différés il y aura lors de l'exécution dans le problème réel que j'essaie de résoudre.

adamjford
la source
Ajout d'une nouvelle réponse, plus simple, pour cette très vieille question ci-dessous. Vous n'avez pas besoin d'utiliser un tableau ou pas $.when.applydu tout pour obtenir le même résultat.
Fin du codage
annulé le sujet de la question, car il était trop spécifique (ce n'est pas seulement un problème AJAX)
Alnitak

Réponses:

732

Pour passer un tableau de valeurs à n'importe quelle fonction qui s'attend normalement à ce qu'il s'agisse de paramètres distincts, utilisez Function.prototype.apply, dans ce cas, vous avez besoin de:

$.when.apply($, my_array).then( ___ );

Voir http://jsfiddle.net/YNGcm/21/

Dans ES6, vous pouvez utiliser l' ... opérateur d'étalement à la place:

$.when(...my_array).then( ___ );

Dans les deux cas, puisqu'il est peu probable que vous sachiez à l'avance combien de paramètres formels le .thengestionnaire aura besoin, ce gestionnaire devra traiter le argumentstableau afin de récupérer le résultat de chaque promesse.

Alnitak
la source
4
Cela fonctionne, génial. :) Je suis étonné de ne pas avoir été en mesure de réaliser un changement aussi simple via Google!
adamjford
9
c'est parce que c'est une méthode générique, non spécifique à $.when- f.apply(ctx, my_array)va appeler favec this == ctxet les arguments sont mis au contenu de my_array.
Alnitak
4
@Alnitak: Je suis un peu gêné de ne pas connaître cette méthode, vu depuis combien de temps j'écris JavaScript maintenant!
adamjford
5
FWIW, le lien dans la réponse d'Eli à une question précédente avec une discussion sur le passage $vs nullcomme premier paramètre mérite d'être lu. Dans ce cas particulier, cela n'a pas d'importance.
Alnitak
4
@Alnitak: Oui, mais il $faut moins taper que nullet vous êtes en sécurité lorsque l' $.whenimplémentation change (pas que ce soit probable dans ce cas mais pourquoi ne pas rester thisinchangé par défaut).
Tomasz Zieliński
109

Les solutions de contournement ci-dessus (merci!) Ne résolvent pas correctement le problème de récupération des objets fournis à la resolve()méthode du différé car jQuery appelle les done()et fail()rappels avec des paramètres individuels, pas un tableau. Cela signifie que nous devons utiliser le argumentspseudo-tableau pour obtenir tous les objets résolus / rejetés renvoyés par le tableau de différés, ce qui est laid:

$.when.apply($,deferreds).then(function() {
     var objects=arguments; // The array of resolved objects as a pseudo-array
     ...
};

Puisque nous avons passé un tableau de différés, il serait bien de récupérer un tableau de résultats. Il serait également intéressant de récupérer un tableau réel au lieu d'un pseudo-tableau afin que nous puissions utiliser des méthodes comme Array.sort().

Voici une solution inspirée de la méthode when.jswhen.all() qui résout ces problèmes:

// Put somewhere in your scripting environment
if (typeof jQuery.when.all === 'undefined') {
    jQuery.when.all = function (deferreds) {
        return $.Deferred(function (def) {
            $.when.apply(jQuery, deferreds).then(
                function () {
                    def.resolveWith(this, [Array.prototype.slice.call(arguments)]);
                },
                function () {
                    def.rejectWith(this, [Array.prototype.slice.call(arguments)]);
                });
        });
    }
}

Maintenant, vous pouvez simplement passer un tableau de différés / promesses et récupérer un tableau d'objets résolus / rejetés dans votre rappel, comme ceci:

$.when.all(deferreds).then(function(objects) {
    console.log("Resolved objects:", objects);
});
canard croustillant
la source
6
Vous voudrez peut-être utiliser ResolveWith et RejeterWith juste pour obtenir les mêmes différés originaux que 'this' deferred.resolveWith (this, [Array.prototype.slice.call (arguments)]) etc
Jamie Pate
1
Il y a juste un petit problème avec votre code, quand il n'y a qu'un élément dans le tableau, le tableau de résultats retourne juste ce résultat, au lieu d'un tableau avec un seul élément (ce qui cassera le code qui attend un tableau). Pour le corriger, utilisez cette fonction var toArray = function (args) { return deferreds.length > 1 ? $.makeArray(args) : [args]; }au lieu de Array.prototype.slice.call.
Luan Nico
Hm, cela ne semble pas attraper de 404.
t.mikael.d
Trouvé la raison, .fail devrait être .reject à la place - donc il peut attraper 404.
t.mikael.d
38

Vous pouvez appliquer la whenméthode à votre tableau:

var arr = [ /* Deferred objects */ ];

$.when.apply($, arr);

Comment travaillez-vous avec un tableau de jQuery différés?

Eli
la source
En fait, j'ai vu cette question, mais je suppose que tous les détails supplémentaires dans cette question ont fait que la réponse à mon problème (qui était juste là-dedans) a survolé ma tête.
adamjford
1
@adamjford, si cela vous fait vous sentir mieux, j'ai trouvé votre question plus facile à consommer (et d'abord sur ma recherche Google particulière pour ce problème précis).
patridge
@patridge: Heureux d'apprendre que cela vous a aidé!
adamjford
C'est une excellente réponse, mais je ne savais pas comment cela s'appliquait à l'exemple de la question d'origine. Après avoir consulté la question liée, il est devenu clair que la ligne "$ .when (différés) .done (fonction () {" devrait simplement être remplacée par "$ .when.apply ($, différés) .done (fonction () { ". N'est-ce pas?
Garland Pope
7

Lorsque vous appelez plusieurs appels AJAX parallèles, vous avez deux options pour gérer les réponses respectives.

  1. Utiliser un appel AJAX synchrone / l'un après l'autre / non recommandé
  2. Utilisez un Promises'tableau et $.whenqui accepte promises et son rappel .doneest appelé lorsque tous les promises sont renvoyés avec succès avec les réponses respectives.

Exemple

function ajaxRequest(capitalCity) {
   return $.ajax({
        url: 'https://restcountries.eu/rest/v1/capital/'+capitalCity,
        success: function(response) {
        },
        error: function(response) {
          console.log("Error")
        }
    });
}
$(function(){
   var capitalCities = ['Delhi', 'Beijing', 'Washington', 'Tokyo', 'London'];
   $('#capitals').text(capitalCities);

   function getCountryCapitals(){ //do multiple parallel ajax requests
      var promises = [];   
      for(var i=0,l=capitalCities.length; i<l; i++){
            var promise = ajaxRequest(capitalCities[i]);
            promises.push(promise);
      }
  
      $.when.apply($, promises)
        .done(fillCountryCapitals);
   }
  
   function fillCountryCapitals(){
        var countries = [];
        var responses = arguments;
        for(i in responses){
            console.dir(responses[i]);
            countries.push(responses[i][0][0].nativeName)
        }  
        $('#countries').text(countries);
   }
  
   getCountryCapitals()
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div>
  <h4>Capital Cities : </h4> <span id="capitals"></span>
  <h4>Respective Country's Native Names : </h4> <span id="countries"></span>
</div>

vinayakj
la source
1
votre réponse dépasse les limites, tout comme votre modification du titre de la question. L'OP savait déjà comment effectuer les appels AJAX et obtenir un tableau d'objets différés. Le seul point de la question était de savoir comment transmettre ce tableau à $.when.
Alnitak du
5
Je pensais qu'expliquer en détail avec un exemple serait mieux, avec les options disponibles. Et pour cela, je ne pense pas que le downvote était nécessaire.
vinayakj
2
le downvote était pour 1. suggérant même la synchronisation (quoique avec une recommandation de ne pas le faire) 2. le code de mauvaise qualité dans les exemples (y compris for ... insur un tableau?!)
Alnitak
1
1. D'accord, aurait dû avoir (not recommended)2.Non d'accord - for ... inest ok parce que le tableau contient uniquement les propriétés qui ont besoin (pas de propriétés supplémentaires). Thanx de toute façon
vinayakj
1
re: 2 - le problème est qu'il pourrait être copié par d'autres personnes qui ne peuvent pas faire cette garantie, ou qui ont été assez stupides pour y ajouter Array.prototype. Dans tous les cas, pour un code non critique pour les performances, il serait préférable d'utiliser à la .mapplace d'une boucle for/ push, par exemple var promises = capitalCities.map(ajaxRequest); $.when.apply($, promises).then(fillCountryCapitals)- travail effectué.
Alnitak
6

Comme alternative simple, qui ne nécessite pas $.when.applyou un array, vous pouvez utiliser le modèle suivant pour générer une seule promesse pour plusieurs promesses parallèles:

promise = $.when(promise, anotherPromise);

par exemple

function GetSomeDeferredStuff() {
    // Start with an empty resolved promise (or undefined does the same!)
    var promise;
    var i = 1;
    for (i = 1; i <= 5; i++) {
        var count = i;

        promise = $.when(promise,
        $.ajax({
            type: "POST",
            url: '/echo/html/',
            data: {
                html: "<p>Task #" + count + " complete.",
                delay: count / 2
            },
            success: function (data) {
                $("div").append(data);
            }
        }));
    }
    return promise;
}

$(function () {
    $("a").click(function () {
        var promise = GetSomeDeferredStuff();
        promise.then(function () {
            $("div").append("<p>All done!</p>");
        });
    });
});

Remarques:

  • J'ai compris celui-ci après avoir vu une chaîne promettre séquentiellement, en utilisant promise = promise.then(newpromise)
  • L'inconvénient est qu'il crée des objets de promesse supplémentaires dans les coulisses et tous les paramètres passés à la fin ne sont pas très utiles (car ils sont imbriqués dans des objets supplémentaires). Pour ce que vous voulez, c'est court et simple.
  • L'avantage est qu'il ne nécessite aucune baie ni gestion de baie.
Fin du codage
la source
2
Corrigez-moi si je me trompe, mais votre approche imbrique efficacement $ .when ($ .when ($ .when (...))) de sorte que vous vous retrouvez récursivement imbriqué 10 niveaux en profondeur s'il y a 10 itérations. Cela ne semble pas très parallèle car vous devez attendre que chaque niveau renvoie la promesse imbriquée d'un enfant avant de pouvoir retourner sa propre promesse - je pense que l'approche de tableau dans la réponse acceptée est beaucoup plus propre car elle utilise le comportement de paramètre flexible intégré à la méthode $ .when ().
Anthony McLin
@AnthonyMcLin: ceci est destiné à fournir une alternative plus simple au codage, pas de meilleures performances (ce qui est négligeable avec la plupart du codage Async), comme cela se fait avec les then()appels de chaînage de manière similaire. Le comportement avec $.whenest d'agir comme il est parallèle (non enchaîné). Veuillez l'essayer avant de jeter une alternative utile car cela fonctionne :)
Gone Coding
2
@Alnitak: Chevaux pour les cours. Vous avez certainement droit à un avis, mais vous ne l'avez évidemment pas utilisé vous-même. Ma propre opinion est basée sur des utilisations pratiques de cette technique. Cela fonctionne et a des utilisations, alors pourquoi jeter un outil de la boîte à outils basé sur des exagérations comme "des charges de mises en garde" (une) et "ne résout rien" (pas vrai - il élimine le traitement du tableau et simplifie le chaînage de promesses parallèles où le retour les valeurs ne sont pas nécessaires, ce qui, comme vous devez le savoir, est rarement utilisé dans les cas de traitement parallèle). Les votes descendants sont censés être pour "cette réponse n'est pas utile" :)
Fin du codage
1
Salut @GoneCoding. Puis-je vous demander de ne pas ajouter de commentaire de vote à vos réponses? Cela convient aux commentaires, mais sinon c'est le bruit qui distrait du contenu par ailleurs bon. Merci.
halfer
1
@halfer: Je ne poste plus mais je suis ennuyé par l'ignorance affichée à quelque chose d'original. Garder toutes les nouvelles idées pour moi de nos jours :)
Gone Coding
4

Je veux en proposer un autre en utilisant $ .each:

  1. Nous pouvons déclarer la fonction ajax comme:

    function ajaxFn(someData) {
        this.someData = someData;
        var that = this;
        return function () {
            var promise = $.Deferred();
            $.ajax({
                method: "POST",
                url: "url",
                data: that.someData,
                success: function(data) {
                    promise.resolve(data);
                },
                error: function(data) {
                    promise.reject(data);
                }
            })
            return promise;
        }
    }
  2. Partie du code où nous créons un tableau de fonctions avec ajax pour envoyer:

    var arrayOfFn = [];
    for (var i = 0; i < someDataArray.length; i++) {
        var ajaxFnForArray = new ajaxFn(someDataArray[i]);
        arrayOfFn.push(ajaxFnForArray);
    }
  3. Et les fonctions d'appel avec l'envoi ajax:

    $.when(
        $.each(arrayOfFn, function(index, value) {
            value.call()
        })
    ).then(function() {
            alert("Cheer!");
        }
    )
Volodymyr Yasinskyi
la source
1

Si vous transpilez et avez accès à ES6, vous pouvez utiliser la syntaxe de propagation qui applique spécifiquement chaque élément itérable d'un objet comme argument discret, exactement comme $.when()il en a besoin.

$.when(...deferreds).done(() => {
    // do stuff
});

Lien MDN - Syntaxe de propagation

relique
la source
0

Si vous utilisez angularJS ou une variante de la bibliothèque Q promise, vous disposez d'une .all()méthode qui résout ce problème exact.

var savePromises = [];
angular.forEach(models, function(model){
  savePromises.push(
    model.saveToServer()
  )
});

$q.all(savePromises).then(
  function success(results){...},
  function failed(results){...}
);

voir l'API complète:

https://github.com/kriskowal/q/wiki/API-Reference#promiseall

https://docs.angularjs.org/api/ng/service/$q

mastaBlasta
la source
4
C'est complètement hors de propos.
Benjamin Gruenbaum
@BenjaminGruenbaum Comment cela? Toutes les bibliothèques de promesses javascript partagent une API similaire, et il n'y a rien de mal à montrer les différentes implémentations. J'ai atteint cette page à la recherche d'une réponse pour angulaire, et je soupçonne que de nombreux autres utilisateurs atteindront cette page et ne seront pas nécessairement dans un environnement uniquement jquery.
mastaBlasta
2
À savoir, parce que les promesses de jQuery ne partagent pas cette API, cela est complètement inapproprié comme réponse sur Stack Overflow - il y a des réponses similaires pour Angular et vous pouvez y demander. (Sans parler, vous devriez .mapici, mais eh bien).
Benjamin Gruenbaum
0

J'ai eu un cas très similaire où je publiais dans une boucle each puis définissais le balisage html dans certains champs à partir des numéros reçus de l'ajax. J'ai ensuite dû faire une somme des valeurs (maintenant mises à jour) de ces champs et les placer dans un champ total.

Ainsi, le problème était que j'essayais de faire une somme sur tous les numéros mais aucune donnée n'était encore revenue des appels asynchrones ajax. J'avais besoin de compléter cette fonctionnalité dans quelques fonctions pour pouvoir réutiliser le code. Ma fonction externe attend les données avant d'aller ensuite faire des choses avec le DOM entièrement mis à jour.

    // 1st
    function Outer() {
        var deferreds = GetAllData();

        $.when.apply($, deferreds).done(function () {
            // now you can do whatever you want with the updated page
        });
    }

    // 2nd
    function GetAllData() {
        var deferreds = [];
        $('.calculatedField').each(function (data) {
            deferreds.push(GetIndividualData($(this)));
        });
        return deferreds;
    }

    // 3rd
    function GetIndividualData(item) {
        var def = new $.Deferred();
        $.post('@Url.Action("GetData")', function (data) {
            item.html(data.valueFromAjax);
            def.resolve(data);
        });
        return def;
    }
Cameron Forward
la source