web-dev-qa-db-fra.com

Les différés jQuery peuvent-ils être annulés?

J'ai une situation où je veux annuler un report. Le différé est associé à un appel ajax.

Pourquoi j'utilise des reports

Je n'utilise pas les objets xhr normaux retournés par $ .ajax. J'utilise jsonp, ce qui signifie que je ne peux pas utiliser les codes d'état HTTP pour la gestion des erreurs et que je dois les incorporer dans les réponses. Les codes sont ensuite examinés et un objet différé associé est marqué comme résolu ou rejeté en conséquence. J'ai une fonction api personnalisée qui fait cela pour moi.

function api(options) {
  var url = settings('api') + options.url;
  var deferred = $.Deferred(function(){
    this.done(options.success);
    this.fail(options.error);
  });
  $.ajax({
    'url': url,
    'dataType':'jsonp',
    'data': (options.noAuth == true) ? options.data : $.extend(true, getAPICredentials(), options.data)
  }).success(function(jsonReturn){
    // Success
    if(hasStatus(jsonReturn, 'code', 200)) {
      deferred.resolveWith(this, [jsonReturn]);
    } 
    // Failure
    else {
      deferred.rejectWith(this, [jsonReturn]);
    }
  });

  return deferred;
}

Pourquoi je veux annuler le report

Il existe un champ de saisie qui sert de filtre à une liste et met automatiquement à jour la liste une demi-seconde après la fin de la saisie. Comme il est possible que deux appels ajax soient en attente à la fois, je dois annuler l'appel précédent pour m'assurer qu'il ne revient pas après le second et afficher les anciennes données.

Solutions que je n'aime pas

  • Je ne veux pas rejeter le différé car cela déclenchera les gestionnaires attachés avec .fail().
  • Je ne peux pas l'ignorer car il sera automatiquement marqué comme résolu ou rejeté au retour de l'ajax.
  • La suppression du différé provoquera une erreur lorsque l'appel ajax reviendra et tentera de marquer le différé comme résolu ou rejeté.

Que devrais-je faire?

Existe-t-il un moyen d'annuler le report ou de supprimer les gestionnaires attachés?

Des conseils sur la façon de réparer ma conception sont les bienvenus, mais la préférence sera donnée à la recherche d'un moyen de supprimer les gestionnaires ou de les empêcher de se déclencher.

40
user879121

En regardant dans le doc et le code jQuery, je ne vois aucun moyen d'annuler un jQuery différé.

Au lieu de cela, vous avez probablement besoin d'un moyen dans votre gestionnaire resolveWith pour savoir qu'un appel ajax suivant a déjà été déclenché et cet appel ajax doit ignorer son résultat. Vous pouvez le faire avec un compteur à incrémentation globale. Au début de votre appel ajax, vous incrémentez le compteur, puis vous saisissez la valeur dans une variable locale ou la placez comme propriété sur l'objet ajax. Dans votre gestionnaire resolveWith, vous vérifiez si le compteur a toujours la même valeur qu'au début de votre appel ajax. Sinon, vous ignorez le résultat. Si c'est le cas, aucun nouvel appel ajax n'a été déclenché, vous pouvez donc traiter le résultat.

Alternativement, vous pouvez refuser de lancer un nouvel appel ajax alors qu'un est déjà en vol, vous n'en avez donc jamais eu plus d'un à la fois. Lorsque l'un se termine, vous pouvez soit simplement utiliser ce résultat, soit déclencher le suivant si vous le souhaitez.

16
jfriend00

Bien que vous ne puissiez pas "annuler" un différé comme vous le souhaitez, vous pouvez créer une fermeture simple pour garder une trace du dernier appel ajax via $ .ajax renvoyant un objet jqXHR. En faisant cela, vous pouvez simplement abandonner () l'appel lorsqu'un nouveau jqXHR entre en jeu si le dernier n'est pas terminé. Dans le cas de votre code, il rejettera le jqXHR et laissera le différé ouvert à supprimer comme vous le vouliez initialement.

var api = (function() {
    var jqXHR = null;

    return function(options) {
        var url = options.url;

        if (jqXHR && jqXHR.state() === 'pending') {
            //Calls any error / fail callbacks of jqXHR
            jqXHR.abort();
        }

        var deferred = $.Deferred(function() {
            this.done(options.success);
            this.fail(options.error);
        });

        jqXHR = $.ajax({
             url: url,
             data: options.toSend,
             dataType: 'jsonp'
        });

        jqXHR.done(function(data, textStatus, jqXHR) {
            if (data.f && data.f !== "false") {
                deferred.resolve();
            } else {
                deferred.reject();
            }
        });

        //http://api.jquery.com/deferred.promise/  
        //keeps deferred's state from being changed outside this scope      
        return deferred.promise();
    };
})();

Je l'ai posté sur jsfiddle . Si vous souhaitez le tester. Set timeout est utilisé en combinaison avec jsfiddles delayer pour simuler un appel en cours d'interruption. Vous aurez besoin d'un navigateur compatible avec la console pour voir les journaux.

Sur une note latérale, basculez les méthodes .success (), .error () et complete () sur les méthodes différées done (), fail () et always (). Via jquery/ajax

Avis de dépréciation: les rappels jqXHR.success (), jqXHR.error () et jqXHR.complete () seront dépréciés dans jQuery 1.8. Pour préparer votre code pour leur suppression éventuelle, utilisez jqXHR.done (), jqXHR.fail () et jqXHR.always () à la place comme plus récent

7
Ryan Q

JustinY: on dirait que vous êtes déjà très proche de ce que vous voulez. Vous utilisez déjà deux différés (interne-> ajax et externe -> $ .Deferred ()). Vous utilisez ensuite le différé interne pour décider comment résoudre le différé externe en fonction de certaines conditions.

Eh bien, alors ne résolvez pas du tout le différé externe lorsque vous ne le souhaitez pas (peut-être que vous avez une variable booléenne qui sert de porte à bascule pour permettre au dfd interne de résoudre/rejeter du tout). Il ne se passera rien de mal: les gestionnaires que vous avez attachés à cette fonction entière ne se déclencheront pas. Exemple dans votre fonction de réussite intérieure:

if(gateOpen){
  gateOpen = false;
  if(hasStatus(jsonReturn, 'code', 200)) {
    deferred.resolveWith(this, [jsonReturn]);
  }
  else {
    deferred.rejectWith(this, [jsonReturn]);
  }
}

Une autre logique de l'application décidera du moment où gateOpen sera remis à true (une sorte de délai d'attente _.throttle () ou _.debounce (), interaction avec l'utilisateur, tout ce que vous voulez) .Si vous vouliez suivre ou annuler d'autres demandes dans le reste de cette fonction, vous pouvez le faire aussi. Mais la chose de base est que vous n'avez pas à résoudre OR rejetez cet élément externe différé. Et cela revient à l'annuler, même si vous n'annulez pas/n'interrompez pas l'élément interne.

1
Dtipson

J'ai créé une cale qui ajoute de façon transparente la possibilité d'annuler les objets différés et les demandes ajax.

En bref, une fois qu'un objet différé a été annulé, les résolutions/rejets sont complètement ignorés et le state devient "annulé".

Selon jQuery.com, "Une fois que l'objet est entré dans l'état résolu ou rejeté, il reste dans cet état." Par conséquent, les tentatives d'annulation sont ignorées une fois qu'un objet différé est résolu ou rejeté.

(function () {
    originals = {
        deferred: $.Deferred,
        ajax: $.ajax
    };

    $.Deferred = function () {

        var dfr = originals.deferred(),
            cancel_dfr = originals.deferred();

        dfr.canceled = false;

        return {
            cancel: function () {
                if (dfr.state() == 'pending') {
                    dfr.canceled = true;
                    cancel_dfr.resolve.apply(this, arguments);
                }
                return this;
            },

            canceled: cancel_dfr.done,

            resolve: function () {
                if ( ! dfr.canceled) {
                    dfr.resolve.apply(dfr, arguments);
                    return this;
                }
            },

            resolveWith: function () {
                if ( ! dfr.canceled) {
                    dfr.resolveWith.apply(dfr, arguments);
                    return this;
                }
            },

            reject: function () {
                if ( ! dfr.canceled) {
                    dfr.reject.apply(dfr, arguments);
                    return this;
                }
            },

            rejectWith: function () {
                if ( ! dfr.canceled) {
                    dfr.rejectWith.apply(dfr, arguments);
                    return this;
                }
            },

            notify: function () {
                if ( ! dfr.canceled) {
                    dfr.notify.apply(dfr, arguments);
                    return this;
                }
            },

            notifyWith: function () {
                if ( ! dfr.canceled) {
                    dfr.notifyWith.apply(dfr, arguments);
                    return this;
                }
            },

            state: function () {
                if (dfr.canceled) {
                    return "canceled";
                } else {
                    return dfr.state();
                }
            },

            always   : dfr.always,
            then     : dfr.then,
            promise  : dfr.promise,
            pipe     : dfr.pipe,
            done     : dfr.done,
            fail     : dfr.fail,
            progress : dfr.progress
        };
    };


    $.ajax = function () {

        var dfr = $.Deferred(),
            ajax_call = originals.ajax.apply(this, arguments)
                .done(dfr.resolve)
                .fail(dfr.reject),

            newAjax = {},

            ajax_keys = [
                "getResponseHeader",
                "getAllResponseHeaders",
                "setRequestHeader",
                "overrideMimeType",
                "statusCode",
                "abort"
            ],

            dfr_keys = [
                "always",
                "pipe",
                "progress",
                "then",
                "cancel",
                "state",
                "fail",
                "promise",
                "done",
                "canceled"
            ];

        _.forEach(ajax_keys, function (key) {
            newAjax[key] = ajax_call[key];
        });

        _.forEach(dfr_keys, function (key) {
            newAjax[key] = dfr[key];
        });

        newAjax.success = dfr.done;
        newAjax.error = dfr.fail;
        newAjax.complete = dfr.always;

        Object.defineProperty(newAjax, 'readyState', {
            enumerable: true,
            get: function () {
                return ajax_call.readyState;
            },
            set: function (val) {
                ajax_call.readyState = val;
            }
        });

        Object.defineProperty(newAjax, 'status', {
            enumerable: true,
            get: function () {
                return ajax_call.status;
            },
            set: function (val) {
                ajax_call.status = val;
            }
        });

        Object.defineProperty(newAjax, 'statusText', {
            enumerable: true,
            get: function () {
                return ajax_call.statusText;
            },
            set: function (val) {
                ajax_call.statusText = val;
            }
        });

        // canceling an ajax request should also abort the call
        newAjax.canceled(ajax_call.abort);

        return newAjax;
    };
});

Une fois ajouté, vous pouvez annuler un appel ajax:

var a = $.ajax({
        url: '//example.com/service/'
    });

a.cancel('the request was canceled');

// Now, any resolutions or rejections are ignored, and the network request is dropped.

..ou un simple objet différé:

var dfr = $.Deferred();

dfr
    .done(function () {
        console.log('Done!');
    })
    .fail(function () {
        console.log('Nope!');
    });

dfr.cancel(); // Now, the lines below are ignored. No console logs will appear.

dfr.resolve();
dfr.reject();
1
Joshua Hansen