web-dev-qa-db-fra.com

jQuery.when - Rappel pour quand TOUS les différés ne sont plus "non résolus" (résolus ou rejetés)?

Lorsque plusieurs objets différés sont passés à jQuery.when , la méthode retourne la promesse d'un nouvel objet différé "maître" qui suit l'état d'agrégat de tous les différés qu'il a été transmis.

La méthode sera soit

  1. résoudre son maître différé dès que TOUTES les différées résolvent, ou
  2. rejette son maître différé dès qu'un des différés est rejeté.

Si le maître différé est résolu (c'est-à-dire TOUTES les résolutions différées), les valeurs résolues de tous les différés transmis à jQuery.when sont transmises. Par exemple, lorsque les demandes différées sont des demandes jQuery.ajax (), les arguments seront les objets jqXHR pour les demandes, dans l'ordre indiqué dans la liste des arguments:

$.when( $.getJSON('foo'), $.getJSON('bar') ).done(function(foo, bar) {

    // foo & bar are jqXHR objects for the requests

});

Dans le cas multiple de différés où l'un des différés est rejeté, jQuery.when LIRE IMMÉDIATEMENT les rappels en échec pour son maître différé, même si certains des différés peuvent toujours être non résolus à ce stade:

$.when( $.getJSON('foo'), $.getJSON('bar') ).fail(function(req) {

    // req is the jqXHR object for one of the failed requests

});

J'ai besoin de déclencher un rappel lorsque tous les différés sont passés à jQuery.when n'est plus «non résolu» (c'est-à-dire qu'ils sont tous soit «résolus» ou «rejetés»). Je pouvais envoyer des objets JSON avec 200 codes OK (à la place de l'envoi de JSON avec des codes d'état d'erreur 404 Not Found) et déterminer le succès/l'erreur dans la méthode done (), mais je préférerais conserver mon API RESTful. Comment puis-je accomplir cela?

36
Alan Spacer

Je pense que le moyen le plus simple de le faire est de garder un objet Deferred secondaire pour chaque demande AJAX et de s'assurer que que on est toujours résolu:

var d1 = $.Deferred();
var d2 = $.Deferred();

var j1 = $.getJSON(...).complete(d1.resolve);
var j2 = $.getJSON(...).complete(d2.resolve);

$.when(j1, j2).done( only fires if j1 AND j2 are resolved );

$.when(d1, d2).done(function() {
     // will fire when j1 AND j2 are both resolved OR rejected
     // check j1.isResolved() and j2.isResolved() to find which failed
});

Ceci utilise la méthode supplémentaire AJAX .complete() que jQuery ajoute à ses promesses pour les méthodes AJAX, appelées à la fois pour les promesses résolues et rejetées.

NB: d1.resolve fonctionne comme un rappel, il n’a pas besoin d’être inséré dans un bloc function() { ... }.

44
Alnitak

La réponse de @Alnitak est intelligente et m'a aidé à effacer le piratage que j'avais créé dans lequel je résolvais de manière quelque peu artificielle une promesse - quel que soit le résultat sous-jacent - afin que je puisse utiliser «quand» pour regrouper plusieurs demandes et utiliser «terminé». procéder indépendamment de leur succès/échec.

Je "réponds" à la réponse d'Alnitak dans l'espoir de donner une autre utilité à sa suggestion qui soutient un nombre arbitraire de promesses sous-jacentes.

var asyncFunc, entity, entities, $deferred, $deferreds;
// ...
foreach (entity in entities) {
    $deferred = $.Deferred();
    $deferreds.Push($deferred);
    asyncFunc(entity).done(...).fail(...).always($deferred.resolve);
}
// ...
$.when.apply($, $deferreds).done(...)

C'est du pseudo-JavaScript, mais cela devrait indiquer l'approche. Pour un ensemble d'entités de taille arbitraire, créez un différé ($ différé) pour chaque entité et transmettez-le à un tableau ($ différé), effectuez l'appel asynchrone, ajoutez done/fail comme vous le souhaitez, mais incluez toujours un 'toujours' qui résout ce problème. entité de $ différé. NB le 'toujours' reçoit la fonction de résolution du différé et non son invocation.

Le 'when' convertit le tableau $ deferreds en liste d'arguments pour 'when' et, puisque la résolution de cet ensemble de reports est garantie (grâce à always), il est maintenant possible de définir un 'done' qui sera invoqué une fois les appels asynchrones sont terminés, que ces tentatives réussissent ou non.

11
DazWilkin

J'ai récemment créé un plugin qui peut aider. Je l'appelle $.whenAll

Cette extension traite tous les succès et les échecs comme des événements de progrès. Une fois toutes les promesses remplies, la promesse globale est résolue S'il n'y a pas eu d'erreur. Sinon, la promesse globale est rejetée.

$ .whenAll - https://Gist.github.com/4341799 ( tests )

Exemple d'utilisation:

$.whenAll($.getJSON('foo'), $.getJSON('bar'))
  .then(
    doneCallback
    ,failcallback
    // progress callback
    // the only problem is $.ajax.done/fail states call their callbacks 
    // with params in different locations (except for state)
    ,function(data, state, jqXhr) {
      if (state == 'success') {
        // do happy stuff
      }
      else { // error (fail)
        // `data` is actually the jqXhr object for failed requests
        // `jqXhr` is the text of the error "Not Found" in this example
      }
    }
  )
;
9
fearphage

Ma mise en oeuvre:

Code du plugin:

jQuery.whenAll = function (deferreds) {
        var lastResolved = 0;

        var wrappedDeferreds = [];

        for (var i = 0; i < deferreds.length; i++) {
            wrappedDeferreds.Push(jQuery.Deferred());

            deferreds[i].always(function() {
                wrappedDeferreds[lastResolved++].resolve(arguments);
            });
        }

        return jQuery.when.apply(jQuery, wrappedDeferreds).promise();
    };

Pour l'utiliser:

jQuery.whenAll([jQuery.get('/your-resource'), jQuery.get('/your-resource')])
   .done(
       function(result1, result2) {
           console.log(result1[1]);
           console.log(result2[1]);
       });

Découvrez le violon: http://jsfiddle.net/LeoJH/VMQ3F/

6
leojh

Voici un plugin jQuery que j'ai créé en modifiant le code principal de $.when() pour utiliser votre sémantique. A défaut d'un meilleur nom, il s'appelle $.myWhen():

(function($) {
  $.myWhen = function( subordinate /* , ..., subordinateN */ ) {
    var i = 0,
      responseValues = Array.prototype.slice.call( arguments ),
      length = responseValues.length,

      // the count of uncompleted subordinates
      remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0,

      // the master Deferred. If responseValues consist of only a single Deferred, just use that.
      deferred = remaining === 1 ? subordinate : jQuery.Deferred(),

      // Update function for all resolve, reject and progress values
      updateFunc = function( i, contexts, values ) {
        return function( value ) {
          contexts[ i ] = this;
          values[ i ] = arguments.length > 1 ? Array.prototype.slice.call( arguments ) : value;
          if( values === progressValues ) {
            deferred.notifyWith( contexts, values );
          } else if ( !( --remaining ) ) {
            deferred.resolveWith( contexts, values );
          }
        };
      },

      progressValues, progressContexts, responseContexts;

    // add listeners to Deferred subordinates; treat others as resolved
    if ( length > 1 ) {
      progressValues = new Array( length );
      progressContexts = new Array( length );
      responseContexts = new Array( length );
      for ( ; i < length; i++ ) {
        if ( responseValues[ i ] && jQuery.isFunction( responseValues[ i ].promise ) ) {
          responseValues[ i ].promise()
            .always( updateFunc( i, responseContexts, responseValues ) )
            .progress( updateFunc( i, progressContexts, progressValues ) );
        } else {
          --remaining;
        }
      }
    }

    // if we're not waiting on anything, resolve the master
    if ( !remaining ) {
      deferred.resolveWith( responseContexts, responseValues );
    }

    return deferred.promise();
  };
})(jQuery);

Il suffit de mettre ce code juste après le chargement de jQuery et la fonction $.myWhen() sera disponible à côté de $.when(). Tout le reste est exactement identique à 100% sauf la sémantique.

3
hippietrail

J'ai trouvé une solution dans laquelle j'ai 2 demandes en un moment et où je peux accéder à des succès individuels même lorsque l'une des demandes échoue:

        $.when
        (
            $.getJSON(...).then(function (results)
            {
                console.log('SUCCESS REQUEST 1 BY ITSELF', results);
            }),
            $.getJSON(...).then(function (results)
            {
                console.log('SUCCESS REQUEST 2 BY ITSELF', results);
            })
        ).then
        (
            function (results1, results2)
            {
                console.log('BOTH REQUESTS SUCCESSFUL...');
                console.log('results1', results1);
                console.log('results2', results2);
            },
            function (error1, error2)
            {
                console.log('AT LEAST 1 REQUEST FAILED...');
                console.log('error1', error1);
                console.log('error2', error2);                  
            }
        );
0
Tom Schreck

Une amélioration de la solution de Leo Hernandez pour les cas d’utilisation plus généraux qui n’impliquent pas simplement la récupération de ressources d’un serveur, pouvant par exemple inclure des événements déclenchés par des interactions utilisateur, ou des appels asynchrones à l’interface utilisateur jQuery (par exemple, slideUp () et slideDown ()). Voir https://jsfiddle.net/1trucdn3/ pour un cas d'utilisation amélioré.

$.whenAll = function (deferreds) {
    var lastResolved = 0;
    var wrappedDeferreds = [];

    for (var i = 0; i < deferreds.length; i++) {
        wrappedDeferreds.Push($.Deferred());
        if (deferreds[i] && deferreds[i].always) {
            deferreds[i].always(wrappedDeferreds[lastResolved++].resolve);
        } else {
            wrappedDeferreds[lastResolved++].resolve(deferreds[i]);
        }
    }

    return $.when.apply($, wrappedDeferreds).promise();
};

L’amélioration nous permet de passer des valeurs non différées dans l’argument du tableau. C’était quelque chose que vous pouviez faire avec $ .when (). De plus, j'ai nettoyé la sortie que vous obtenez dans la fonction de rappel pour qu'elle soit plus en ligne avec le fonctionnement de la méthode originale $ .when (), au cas où vous voudriez simplement récupérer le résultat, quel que soit le statut. La solution de Leo transmettrait l’ensemble de l’objet différé, que vous devrez ensuite creuser pour trouver les informations dont vous avez besoin.

$.whenAll([1, $.Deferred().resolve("Good"), $.Deferred().reject("Bad")])
    .done(function (result1, result2, result3) {
        // result1 -> 1
        // result2 -> "Good"
        // result3 -> "Bad"
    });
0
baohouse

Les réponses @Alnitak et @DazWilkin sont excellentes! Mais personnellement, je préfère le style fonctionnel, voici donc une version fonctionnelle pour un nombre arbitraire de promesses:

var entities;
// ...
var deferreds = entities.map(function() {
    var deferred = $.Deferred();
    asyncFunc(this).done(...).fail(...).always(deferred.resolve);
    return deferred;
}
// ...
$.when.apply($, deferreds).done(...)

Comparé à @DazWilkin answer, j'utilise la fonction map au lieu de foreach.

0
lagivan