首页 > 如何使用$.ajax()返回的deferred对象

如何使用$.ajax()返回的deferred对象

1.5版本后的jquery使用$.ajax()返回的是deferred对象
大家都知道deferred对象是jquery给出的回调函数的解决方案
ajax请求可以写成以下形式

JavaScript$.ajax('target.html')
.done(function(){})
.fail(function(){});

非常简单明了
但现在情况是返回数据中有一字段规定请求失败与否(不是通信问题的错误)

一般jquery的ajax的使用如下(当然是我比较晚知道这个deferred这个对象,可能大家老早就不这样写了)

javascript$.ajax({
    url: target.html,
    success: function(){},
    error: function(){}
});

改写如下

JavaScriptvar IO = {};
IO.prototype.ajaxGet = function(url, params, cb, error) {
    $.ajax({
        url: url,
        type: 'get',
        data: params,
        success: function(data) {
            data = $.parseJSON(data);
            if(data.state != 10000) {
                error && error(data);
                return false;
            }
            cb && cb(data);
        },
        error: function(XMLHttpRequest, textStatus, errorThrown) {
            //这里是出现请求失败等问题的处理
        }
    });
}

因为与后台约定好当返回state字段值不为10000则算是错误情况
需要到error函数中执行
但是在successerror中要如何获取deferred对象来进行改写
使之可以像本题最上面的写法使用

JavaScriptIO.ajaxGet('target.html', data)
.done(function(data){})
.fail(function(data){});

//看到这个问题马上打开家里的windows去拖了个sublime来怒答

先谈理想,哦不,概念

首先defered模式不是伪promise,而是promise的一个变种。promise的初版Promise/A只有一个必备API then,后来的Promise/B已经具备了defer方法,虽然jQuery的实现细节与其不同,但基本意图是一样的(可以将promise的resolve这件事同样以方法调用的形式解决)

Promise是一个复杂的大家族,jQuery虽然曾经离经叛道,但自从pipe方法被舍弃,归并入then之后,已经是一个相当完整的Promise实现了。并不100%,但如果不考虑一些特殊场景(后详)下,jQuery的实现已经是Promise/A的超集。

再谈实际情况

Ajax业务错误是最最典型的超级适合用Promise解决问题的场景了,因为Promise的then方法有改变promise状态的能力,也就是标准的这句话

The value returned from the callback handler is the fulfillment value for the returned promise.

在更准确一些的Promise A+中,相应的描述也更明确易懂

then must return a promise [3.3].
promise2 = promise1.then(onFulfilled, onRejected);
  If either onFulfilled or onRejected returns a value x, run the Promise Resolution Procedure [[Resolve]](promise2, x).
  If either onFulfilled or onRejected throws an exception e, promise2 must be rejected with e as the reason.
  If onFulfilled is not a function and promise1 is fulfilled, promise2 must be fulfilled with the same value as promise1.
  If onRejected is not a function and promise1 is rejected, promise2 must be rejected with the same reason as promise1.

换句人话,哦不,中文来说,想要改变promise的对象的状态,只要在then的参数回调当中返回相应的值,普通值代表成功,throw代表失败,另一个promise代表透传
jQuery的实现和标准略有区别,首先throw不会改变promise为失败而是直接就throw出去了,然后在失败的分支里返回非promise的值X的时候,最终promise的状态不是【成功,值X】而是【失败,值X】
重新换句人话,就是jQuery里想要扭转promise的状态就只能依靠返回另一种状态的promise对象。

上实际代码

jsfunction ajaxGet(url, params) {
  return $.get(url, params)
    .then(function(response) {
      var obj;
      try {
        obj = $.parseJSON(response);
      } catch(e) {
        return reject({
          state: 123,//比如说123代表返回结果非json
          response: response,
          error: e
        });
      }

      if(obj.state !== 10000) {
        return reject(obj);//直接把整个响应丢出去,如果约定了错误格式,可以在这里整理一下msg之类
      }

      return response;//成功! 如果约定了成功格式,也可以在这里转换

    }, function(xhr) {//这是答主擅自追加的特性,可以不要
      //网络错误

      return reject({
        state: 456,//比如说456代表网络错误
        xhr: xhr//这是jqXHR对象,里面有错误详情
      });
    });
}

function reject(reason) {//构造处在错误状态的promise对象
  var dfr = $.Deferred();
  dfr.reject(reason);
  return dfr.promise();
}

答主的习惯是在这个封装里顺便再给后台一个特殊的状态,比如state===789代表跳转,然后if(obj.state===789) window.location=obj.url;这样,顺便搞定了ajax捅出去以后发现未登录时跳转登录页等等各种需求,都是业务代码不用管的。另外会统一把错误整理成一句话放在比如msg字段里面, 很多时候业务可以在错误的时候直接.fail(function(err) {alert(err.msg || '未知错误')})完事儿

怒答了好久……

最后如果还意犹未尽的同学,不妨看看这几个issue,描述了三年前,chai-as-promise的作者和jquery中相关模块的作者及其他小伙伴们相爱相杀的曲折经历


jQuery 的 deferred 是伪 Promise 对象,你可以直接用原生的 Promise 对象写,一样也非常酸爽:

var IO = {};
IO.get = function(url, params) {
    return new Promise(function(resolve, reject) {
        var xhr = new XMLHttpRequest;
        if( params ) {
            url += '?' + Object.keys(params).map(function(k){
                return [k, encodeURIComponent(params[k])].join('=')
            }).join('&');
        }

        xhr.onreadystatechange = function() {
            if(xhr.readyState != 4) return;
            if(xhr.status === 200) resolve( xhr.response );
            else reject( Error( 'Status Error:' + xhr.status ) );
        }
        xhr.open("GET", url, true);
        xhr.send(null);
    });
}

IO.get(location.href)
.then(function(data) {
    var data = JSON.parse( data );
    if( data.state === 10000 ) return data;
    else return Error("State Error:"+data.state);
})
.then(function(data) {
    console.log(data);
})
.catch(function(error){
    if(error instanceof Error) { throw error }
    else console.log(error)
})  

不,你不能在 success/error 回调中获取 deferred 对象的结果(或者说你已经在它的结果中了),这是因为 deferred 对象包含的是“未来”的结果——在你获取 deferred 对象的时候,它所代表的异步请求还没发生。并且正因为如此,我们才能利用 deferred 对象来去定义未来可能发生的事情,要么 success(resolved),要么 error(rejected)。所以在 success/error 回调中获取 deferred 对象是没有意义的,就好像“在未来中获取代表未来的那个过去对象”。

那么要怎么做?

从大的层面上来讲,API 的设计不够完整。你们可以自定义错误代码/错误信息,但更重要的是如果发生了错误,那就应该同时修改 HTTP 状态码。我记得 jQuery 的 Ajax 请求会拦截非 2xx 的 HTTP 响应,然后走 error 回调,因此如果你们这样做了,那么属于错误的部分就应该在 deferred.fail() 里面去处理。这才是正途。

当然现实总是很傻缺,错误也经常以 200 的形式返回给客户端,所以我们不得不在 success 回调中处理异常。那么使用 deferred 的时候怎么做呢?

deferred 这种模式和 Promise 很像,它也是一个 thenable 对象,它的 then 方法可以让你串联多个函数并依次执行。所以你可以写一个通用的过滤函数来处理返回错误对象的情况,然后 reject 整个 deferred 对象。由于我很久没用 jQuery 了,以下示例代码仅凭印象写下,你就当是伪代码,看个意思就好:

javascriptvar deferred = $.ajax({
    url: url,
    type: 'get',
    data: params,
    ...
});

function processError(response) {
    if (!(response.state == 10000)) {
        // reject deferred 对象,并将状态码传给 .fail() 的回调
        return deferred.reject(response.state);
    }

    deferred.resolve(response);    // 这一句可能不需要,细节记不清了
}

deferred
    .then(processError)            // 调用上面的函数,过滤异步请求的结果
    .done(processYourBussiness)    // 处理正常逻辑
    .fail(errorHandler);           // 处理异常逻辑

你可能会想不要 errorHandler 转而直接在 processError 里处理异常,但是记住我们之所以这么做,其实就是在“模拟”正确的模式,用 processError 来代替本来应该发生的 HTTP Status 不等于 2xx 的情形。这样一来,无论 API 那里是否处理正确,客户端总是走在正确的轨道上,将来若是 API 更改,处理了正确的状态码,那就只需要去掉 processError 编好,它相当于一个中间件。

若有可能,还是用 Promise 吧,jQuery 的 deferred 实现是有缺陷的。

【热门文章】
【热门文章】