在jQuery1.5中使用deferred对象 着放大镜看Promise


Posted in Javascript onMarch 12, 2011
引言

在那篇经典的关于jQuery1.5中Deferred使用方法介绍的文章中(译文见这里),有下面一段描述:

$.ajax() returns an object packed with other deferred-related methods. I discussed promise(), but you'll also find then(), success(), error(), and a host of others. You don't have access to the complete deferred object, though; only the promise, callback-binding methods, and the isRejected() and isResolved() methods, which can be used to check the state of the deferred.
But why not return the whole object? If this were the case, it would be possible to muck with the works, maybe pragmatically "resolve" the deferred, causing all bound callbacks to fire before the AJAX request had a chance to complete. Therefore, to avoid potentially breaking the whole paradigm, only return the dfd.promise().

这段话非常令人费解,我也是看了几遍才看明白。大致的意思是:
$.ajax()返回一个对象(jqXHR,这是对原生的XMLHttpRequest的封装),这个对象包含了deferred相关的函数,比如promise(), then(), success(), error(), isRejected(), isResolved()。
但是你发现没,这里面没有resolve(), resolveWith(), reject(), rejectWith() 几个函数,而这几个函数才是用来改变deferred对象流程。也就是说$.ajax()返回了一个只读的deferred对象

下面erichynds改用反问的语气提出,为什么不返回完整的deferred对象,而只返回只读的deferred对象?
如果返回完整的deferred对象,那么外部程序就能随意的触发deferred对象的回调函数,很有可能在AJAX请求结束前就触发了回调函数(resolve),这就是与AJAX本身的逻辑相违背了。
所以为了避免不经意间改变任务的内部流程,我们应该只返回deferred的只读版本(dfd.promise())。

为了说明$.ajax()和$.Deferred()返回的deferred对象的不同,请看下面的例子:

// deferred对象所有的方法数组 
var methods = 'done,resolveWith,resolve,isResolved,then,fail,rejectWith,reject,isRejected,promise'.split(','), 
method, 
ajaxMethods = [], 
onlyInDeferredMethods = []; 
for (method in $.ajax()) { 
if ($.inArray(method, methods) !== -1) { 
ajaxMethods.push(method); 
} 
} for (method in $.Deferred()) { 
if ($.inArray(method, methods) !== -1 && $.inArray(method, ajaxMethods) === -1) { 
onlyInDeferredMethods.push(method); 
} 
} 
// 存在于$.Deferred(),但是不存在于$.ajax()的deferred相关方法列表为: 
// ["resolveWith", "resolve", "rejectWith", "reject"] 
console.log(onlyInDeferredMethods);

反面教材
如果$.ajax()返回的对象包含resolve(), resolveWith(),可能会产生哪些影响呢?
我们还是用例子也说明,首先看看erichynds原文的第一个例子:
// $.get, 异步的AJAX请求 
var req = $.get('./sample.txt').success(function (response) { 
console.log('AJAX success'); 
}).error(function () { 
console.log('AJAX error'); 
}); // 添加另外一个AJAX回调函数,此时AJAX或许已经结束,或许还没有结束 
// 由于$.ajax内置了deferred的支持,所以我们可以这样写 
req.success(function (response) { 
console.log('AJAX success2'); 
}); 
console.log('END');

执行结果为:
END -> AJAX success -> AJAX success2

下面修改jQuery1.5源代码,为$.ajax()的返回值添加resolve()和resolveWith()函数:

// Attach deferreds 
deferred.promise( jqXHR ); 
jqXHR.success = jqXHR.done; 
jqXHR.error = jqXHR.fail; 
jqXHR.complete = completeDeferred.done; 
// 下面两行是我们手工增加的,jQuery源代码中没有 
jqXHR.resolve = deferred.resolve; 
jqXHR.resolveWith = deferred.resolveWith;

然后,执行下面代码:
// $.get, 异步的AJAX请求 
var req = $.get('./sample.txt').success(function (response) { 
console.log('AJAX success'); 
}).error(function () { 
console.log('AJAX error'); 
}); req.resolve(); 
// 添加另外一个AJAX回调函数,此时AJAX或许已经结束,或许还没有结束 
// 由于$.ajax内置了deferred的支持,所以我们可以这样写 
req.success(function (response) { 
console.log('AJAX success2'); 
}); 
console.log('END');

此时的执行结果为:
AJAX success -> AJAX success2 -> END

也就是说,在真实的AJAX请求结束之前,success的回调函数就已经被触发了,出现错误。

为了更清楚的看清这一切,我们手工给success回调函数传递一些伪造的参数:

// $.get, 异步的AJAX请求 
var req = $.get('./sample.txt').success(function (response) { 
console.log('AJAX success(' + response + ')'); 
}); req.resolve('Fake data'); 
// 添加另外一个AJAX回调函数,此时AJAX或许已经结束,或许还没有结束 
// 由于$.ajax内置了deferred的支持,所以我们可以这样写 
req.success(function (response) { 
console.log('AJAX success2(' + response + ')'); 
}); 
console.log('END');

此时的执行结果为:
AJAX success(Fake data) -> AJAX success2(Fake data) -> END

代码分析
在深入jQuery代码之前,先来看看jQuery.promise的文档:
The deferred.promise() method allows an asynchronous function to prevent other code from interfering with the progress or status of its internal request. The Promise exposes only the Deferred methods needed to attach additional handlers or determine the state (then, done, fail, isResolved, and isRejected), but not ones that change the state (resolve, reject, resolveWith, and rejectWith).
If you are creating a Deferred, keep a reference to the Deferred so that it can be resolved or rejected at some point. Return only the Promise object via deferred.promise() so other code can register callbacks or inspect the current state.

大致意思是说,deferred.promise()用来阻止其它代码修改异步任务的内部流程。Promise的对象只对外公开添加回调函数和检测状态的函数,而不包含修改状态的函数。
如果你手工创建了一个deferred对象,那么你要维持对这个deferred对象的引用,以此来修改状态触发回调函数。不过你的返回值应该是deferred.promise(),这样外部程序可以添加回调函数或检测状态,而不能修改状态。

至此,大家对promise应该有清晰的认识了。我们再来看下面两段代码,它们完成的功能完全一致:

function getData() { 
return $.get('/foo/'); 
} function showDiv() { 
// 正确代码。推荐做法。 
return $.Deferred(function (dfd) { 
$('#foo').fadeIn(1000, dfd.resolve); 
}).promise(); 
} 
$.when(getData(), showDiv()).then(function (ajaxResult) { 
console.log('The animation AND the AJAX request are both done!'); 
});

function getData() { 
return $.get('/foo/'); 
} function showDiv() { 
// 正确代码。不推荐这么做。 
return $.Deferred(function (dfd) { 
$('#foo').fadeIn(1000, dfd.resolve); 
}); 
} 
$.when(getData(), showDiv()).then(function (ajaxResult) { 
console.log('The animation AND the AJAX request are both done!'); 
});

虽然上面两段代码完成相同的任务,并且似乎第二段代码更加简洁,但是第二段代码却不是推荐的做法。
因为任务(showDiv)本身状态的更改应该保持在任务内部,而不需要对外公开,对外只需要公开一个promise的只读deferred对象就行了。

最后,我们来看看Deferred相关源代码:

// Promise相关方法数组 
promiseMethods = "then done fail isResolved isRejected promise".split( " " ), jQuery.extend( 
// 完备的deferred对象(具有两个回调队列) 
Deferred: function (func) { 
var deferred = jQuery._Deferred(), 
failDeferred = jQuery._Deferred(), 
promise; 
// 添加then, promise 以及出错相关的deferred方法 
jQuery.extend(deferred, { 
then: function (doneCallbacks, failCallbacks) { 
deferred.done(doneCallbacks).fail(failCallbacks); 
return this; 
}, 
fail: failDeferred.done, 
rejectWith: failDeferred.resolveWith, 
reject: failDeferred.resolve, 
isRejected: failDeferred.isResolved, 
// 返回deferred对象的只读副本 
// 如果将obj作为参数传递进去,则promise相关方法将会添加到这个obj上 
promise: function (obj) { 
if (obj == null) { 
if (promise) { 
return promise; 
} 
promise = obj = {}; 
} 
var i = promiseMethods.length; 
while (i--) { 
obj[promiseMethods[i]] = deferred[promiseMethods[i]]; 
} 
return obj; 
} 
}); 
// 确保只有一个回调函数队列可用,也就是说一个任务要么成功,要么失败 
deferred.done(failDeferred.cancel).fail(deferred.cancel); 
// 删除cancel函数 
delete deferred.cancel; 
// 将当前创建的作为参数传递到给定的函数中 
if (func) { 
func.call(deferred, deferred); 
} 
return deferred; 
});

如果你觉得上面的代码阅读比较困难,没关系我写了一个简单的类似代码:
Arr = function () { 
var items = [], 
promise, 
arr = { 
add: function (item) { 
items.push(item); 
}, 
length: function () { 
return items.length; 
}, 
clear: function () { 
items = []; 
}, 
promise: function () { 
if (promise) { 
return promise; 
} 
var obj = promise = {}; 
obj.add = arr.add; 
obj.length = arr.length; 
obj.promise = arr.promise; 
return obj; 
} 
}; 
return arr; 
}

上面代码定义了一个Arr,用来生成一个数组对象,包含一些方法,比如add(), length(), clear(), promise()。
其中promise()返回当前Arr对象的一个副本,只能向其中添加元素,而不能清空内部数组。
var arr = Arr(); 
arr.add(1); 
arr.add(2); 
// 2 
console.log(arr.length()); 
arr.clear(); 
// 0 
console.log(arr.length()); 
var arr = Arr(); 
arr.add(1); 
arr.add(2); 
// 2 
console.log(arr.length()); 
var promise = arr.promise(); 
promise.add(3); 
promise.add(4); 
// 4 
console.log(promise.length()); 
// Error: TypeError: promise.clear is not a function 
promise.clear();

deferred.promise()与deferred.promise().promise()
还记得前面提到的那两个完成相同功能的代码么?
function getData() { 
return $.get('/foo/'); 
} function showDiv() { 
// 这里返回promise()或者直接返回deferred对象,代码都能正确运行。 
return $.Deferred(function (dfd) { 
$('#foo').fadeIn(1000, dfd.resolve); 
}).promise(); 
} 
$.when(getData(), showDiv()).then(function (ajaxResult) { 
console.log('The animation AND the AJAX request are both done!'); 
});

那么你有没有思考过,为什么这两种方式都能运行呢?
如果你深入jQuery的源代码,你会发现$.when(obj1, obj2, ...)在内部实现时会获取obj1.promise():
if ( object && jQuery.isFunction( object.promise ) ) { 
object.promise().then( iCallback(lastIndex), deferred.reject ); 
}

所以我们来看上面showDiv的返回结果:
如果是deferred对象的话,$.when()通过下面方式得到promise:
$.Deferred().promise()

如果是deferred.promise()对象的话,$.when()通过下面方式得到promise:
$.Deferred().promise().promise()

那么是不是说:$.Deferred().promise() === $.Deferred().promise().promise()
我们还是通过示例来验证我们的想法:

var deferred = $.Deferred(), 
promise = deferred.promise(); 
// true 
promise === promise.promise(); 
// true 
promise === promise.promise().promise().promise();

当然,这个结果是推理出来的,如果我们直接看Deferred的源代码,也很容易看出这样的结果:
promise: function (obj) { 
if (obj == null) { 
// 在这里,如果promise已经存在(已经调用过.promise()),就不会重新创建了 
if (promise) { 
return promise; 
} 
promise = obj = {}; 
} 
var i = promiseMethods.length; 
while (i--) { 
obj[promiseMethods[i]] = deferred[promiseMethods[i]]; 
} 
return obj; 
}

总结

1. deferred.promise()返回的是deferred对象的只读属性。
2. 建议任务不要返回deferred对象,而是返回deferred.promise()对象。这样外部就不能随意更改任务的内部流程。
3. deferred.promise() === deferred.promise().promise() (上面我们分别从代码推理,和源代码分析两个角度得到这个结论)

本文由三生石上原创,博客园首发,转载请注明出处。

Javascript 相关文章推荐
JS小框架 fly javascript framework
Nov 26 Javascript
JavaScript 放大镜 放大倍率和视窗尺寸
May 09 Javascript
分享一道笔试题[有n个直线最多可以把一个平面分成多少个部分]
Oct 12 Javascript
解释&&和||在javascript中的另类用法
Jul 28 Javascript
javascript实现校验文件上传控件实例
Apr 20 Javascript
JS实现鼠标滑过链接改变网页背景颜色的方法
Oct 20 Javascript
ui组件之input多选下拉实现方法(带有搜索功能)
Jul 14 Javascript
RGB和YUV 多媒体编程基础详细介绍
Nov 04 Javascript
js手机号4位显示空格,银行卡每4位显示空格效果
Mar 23 Javascript
基于 D3.js 绘制动态进度条的实例详解
Feb 26 Javascript
vue+element模态框中新增模态框和删除功能
Jun 11 Javascript
JS使用H5实现图片预览功能
Sep 30 Javascript
使用jquery插件实现图片延迟加载技术详细说明
Mar 12 #Javascript
Jquery.LazyLoad.js修正版下载,实现图片延迟加载插件
Mar 12 #Javascript
javascript textarea光标定位方法(兼容IE和FF)
Mar 12 #Javascript
JavaScript全局函数使用简单说明
Mar 11 #Javascript
js+css使DIV始终居于屏幕中间 左下 左上 右上 右下的代码集合
Mar 10 #Javascript
始终在屏幕中间显示Div的代码(css+js)
Mar 10 #Javascript
javascript的函数、创建对象、封装、属性和方法、继承
Mar 10 #Javascript
You might like
PHP数据流应用的一个简单实例
2012/09/14 PHP
一个漂亮的php验证码类(分享)
2013/08/06 PHP
6个超实用的PHP代码片段
2015/08/10 PHP
『jQuery』取指定url格式及分割函数应用
2013/04/22 Javascript
javascript遍历控件实例详细解析
2014/01/10 Javascript
jquery让返回的内容显示在特定div里(代码少而精悍)
2014/06/23 Javascript
js数组的基本操作(很全自己整理的)
2014/10/16 Javascript
Javascript 完美运动框架(逐行分析代码,让你轻松了运动的原理)
2015/01/23 Javascript
Jquery中CSS选择器用法分析
2015/02/10 Javascript
你所不了解的javascript操作DOM的细节知识点(一)
2015/06/17 Javascript
深入理解JavaScript中的预解析
2017/01/04 Javascript
javascript设计模式之模块模式学习笔记
2017/02/15 Javascript
Vue异步组件使用详解
2017/04/08 Javascript
JavaScript限定范围拖拽及自定义滚动条应用(3)
2017/05/17 Javascript
通过vue-router懒加载解决首次加载时资源过多导致的速度缓慢问题
2018/04/08 Javascript
vue2.0项目实现路由跳转的方法详解
2018/06/21 Javascript
vue获取元素宽、高、距离左边距离,右,上距离等还有XY坐标轴的方法
2018/09/05 Javascript
如何在项目中使用log4.js的方法步骤
2019/07/16 Javascript
微信小程序开发搜索功能实现(前端+后端+数据库)
2020/03/04 Javascript
在Python程序中操作文件之flush()方法的使用教程
2015/05/24 Python
在Linux系统上部署Apache+Python+Django+MySQL环境
2015/12/24 Python
在python中利用GDAL对tif文件进行读写的方法
2018/11/29 Python
Anaconda 查看、创建、管理和使用python环境的方法
2019/12/03 Python
用python打开摄像头并把图像传回qq邮箱(Pyinstaller打包)
2020/05/17 Python
解决tensorflow模型压缩的问题_踩坑无数,总算搞定
2021/03/02 Python
HTML5 用动画的表现形式装载图像
2016/03/08 HTML / CSS
日本食品网上商店:JaponShop.com
2017/11/28 全球购物
美国波西米亚风格精品店:South Moon Under
2019/10/26 全球购物
Java面试题及答案
2012/09/08 面试题
董事长助理工作职责
2014/06/08 职场文书
先进工作者事迹材料
2014/12/23 职场文书
员工评语范文
2014/12/31 职场文书
2015年幼儿园班务工作总结
2015/05/12 职场文书
民事撤诉申请书范本
2015/05/18 职场文书
员工升职自我评价
2019/03/26 职场文书
python 标准库原理与用法详解之os.path篇
2021/10/24 Python