【问题标题】:Can jQuery deferreds be cancelled?可以取消 jQuery 延迟吗?
【发布时间】:2012-07-16 21:17:26
【问题描述】:

我有一种情况,我想取消延期。 deferred 与 ajax 调用相关联。

为什么我要使用延迟

我不使用 $.ajax 返回的普通 xhr 对象。我正在使用 jsonp,这意味着我不能使用 HTTP 状态代码进行错误处理,而必须将它们嵌入到响应中。然后检查代码并相应地将关联的延迟对象标记为已解决或拒绝。我有一个自定义的 api 函数可以为我执行此操作。

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

为什么要取消延期

有一个输入字段用作列表的过滤器,并会在键入结束后半秒自动更新列表。因为一次可能有两个 ajax 调用未完成,所以我需要取消前一个调用以确保它不会在第二个调用之后返回并显示旧数据。

我不喜欢的解决方案

  • 我不想拒绝延迟,因为这会触发附加.fail() 的处理程序。
  • 我不能忽略它,因为它会在 ajax 返回时自动标记为已解决或拒绝。
  • 当 ajax 调用返回并尝试将 deferred 标记为已解决或已拒绝时,删除 deferred 将导致错误。

我该怎么办?

有没有办法取消延迟或删除任何附加的处理程序?

欢迎就如何修复我的设计提出建议,但我们会优先考虑找到移除处理程序或阻止它们触发的方法。

【问题讨论】:

  • 您能详细说明一下吗?从你说的情况来看,它看起来像一个糟糕的设计
  • 延迟通过或失败。您不能取消它或删除处理程序。也许有更好的方法来做你想做的事情。
  • 您的.fail(jqXHR, textStatus, errorThrown) 处理程序可以检查textStatus 是否“中止”。

标签: javascript jquery ajax jquery-deferred cancellation


【解决方案1】:

我创建了一个 shim,可以无缝添加取消延迟对象和 ajax 请求的功能。

简而言之,一旦延迟对象被取消,决议/拒绝将被完全忽略,state 变为“已取消”。

根据 jQuery.com,“一旦对象进入已解决或已拒绝状态,它就会保持该状态。”因此,一旦延迟对象被解析或拒绝,取消尝试就会被忽略。

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

添加后,您可以取消 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.

..或者一个简单的延迟对象:

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

【讨论】:

    【解决方案2】:

    JustinY:看起来你已经很接近你想要的了。您已经在使用两个延迟(内部-> ajax 和外部-> $.Deferred())。然后,您使用内部延迟来决定如何根据某些条件解决外部延迟。

    好吧,所以当您不想解决外部延迟时,根本不要解决(也许您有一个布尔变量用作切换门,以允许内部 dfd 完全解决/拒绝)。不会发生任何不好的事情:您附加到整个函数的任何处理程序都不会触发。内在成功功能中的示例:

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

    应用程序中的其他一些逻辑将决定 gateOpen 何时设置回 true(某种 _.throttle() 或 _.debounce() 超时、用户交互,无论您想要什么)。如果您想跟踪或在该函数的 else 中取消其他请求,您也可以这样做。但基本的事情是你不必解决或拒绝外部延迟。这与取消它是一样的,即使你不取消/中止内部的。

    【讨论】:

      【解决方案3】:

      虽然您不能像您想要的那样“取消”延迟,但您可以创建一个简单的闭包来通过 $.ajax 返回一个 jqXHR 对象来跟踪最后一次 ajax 调用。通过这样做,如果最后一个没有完成,当一个新的 jqXHR 进入播放时,您可以简单地 abort() 调用。在您的代码的情况下,它将拒绝 jqXHR 并保持延迟打开以按照您最初的意愿被删除。

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

      我已经在jsfiddle 上发布了这个。如果你想测试一下。设置超时与 jsfiddles 延迟器结合使用,以模拟被中断的呼叫。您需要启用控制台的浏览器才能查看日志。

      附带说明,将任何 .success()、.error() 和 complete() 方法切换到延迟方法 done()、fail() 和 always()。通过jquery/ajax

      弃用通知:jqXHR.success()、jqXHR.error() 和 jqXHR.complete() 回调将在 jQuery 1.8 中弃用。要为最终删除准备您的代码,请使用 jqXHR.done()、jqXHR.fail() 和 jqXHR.always() 作为更新的代码

      【讨论】:

      • 我相信jqXHR.abort() 会使用xhr.status === 0 触发fail 函数,因此您可能还想reject() fail / error 函数中的延迟函数。
      • 感谢阿图尔的提醒。提及为什么 OP 无法编辑或提出更改建议会很有帮助。
      【解决方案4】:

      查看 jQuery 文档和代码,我没有看到任何取消 jQuery 延迟的方法。

      相反,您可能需要在您的resolveWith 处理程序中知道后续的ajax 调用已经被触发并且这个ajax 调用应该忽略它的结果。您可以使用全局递增计数器来做到这一点。在 ajax 调用开始时,递增计数器,然后将值抓取到局部变量中或将其作为属性放在 ajax 对象上。在您的resolveWith 处理程序中,您检查计数器是否仍具有与您的ajax 调用开始时相同的值。如果不是,则忽略结果。如果是这样,则不会触发任何新的 ajax 调用,因此您可以处理结果。

      或者,您可以拒绝在一个已经在运行中的新 ajax 调用,这样您一次不会有超过一个在运行中。当一个完成后,您可以直接使用该结果,也可以根据需要触发下一个。

      【讨论】:

      • 我最好选择rejectWith,但建议 +1
      • @zerkms - 这取决于您要在哪里进行重复检测。如果这种情况发生在代码决定是拒绝还是解决的时候,那么rejectWith 可能是一个好主意,带有一个参数表明它由于后续调用而被拒绝。如果 dup 检测发生在已解析的处理程序中,那么它已经被解析,因此拒绝它是不可行的。
      • 另外一个建议:我将{ manuallyAborted: true } 2 参数传递给rejectWith,而不是计数器,因为计数器可能会在真正的多线程环境中导致竞争条件,这不适用于浏览器和 JS,至少现在 ;-)
      • @zerkms - 当结果进来时,计数器用于检测多个运行中的 ajax 调用。如果去掉计数器,你将如何检测到?
      • 好吧,我假设 OP 想要执行 xhr.abort() 来取消不必要的请求。从这个角度来看,我看到更合乎逻辑的地方放置该逻辑 - 是 fail handlers
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-04-15
      • 2019-05-12
      • 1970-01-01
      • 1970-01-01
      • 2012-03-25
      • 2012-01-15
      相关资源
      最近更新 更多