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(){} });
改写如下
JavaScript
var 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函数中执行
但是在success
与error
中要如何获取deferred
对象来进行改写
使之可以像本题最上面的写法使用
如
JavaScript
IO.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对象。
上实际代码
js
function 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的promise不乖,提了这个issue https://github.com/domenic/chai-as-promised/issues/1
- 最后的结局是喜闻乐见的一个困难的决定
if (typeof assertion._obj.pipe === "function") { throw new TypeError("Chai as Promised is incompatible with jQuery's so-called “promises.” Sorry!"); }
-
更精彩的是在jQuery的issue中,相关人士的激烈讨论 http://bugs.jquery.com/ticket/11010
- 幸运的是至少把pipe改名为then这一点还是顺利合入了jQuery1.7,这样看来事情还是一个不错的结局。否则SegmentFault今天还会多100个『jQuery pipe then和Promise的关系』之类的问题,github上还会多很多让jQuery更像Promise的第三方库,甚至连 Promise还能不能进入ES的草案都成疑问
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 了,以下示例代码仅凭印象写下,你就当是伪代码,看个意思就好:
javascript
var 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
实现是有缺陷的。