jQuery的deferred对象
什么是deferred对象
web 开发中常遇到耗时很长的 javascript 操作,既有异步的操作(比如ajax读取服务器数据),也有同步的操作(比如遍历一个大型数组),它们都不是立即能得到结果的。
通常的做法是,为它们指定回调函数(callback)。即事先规定,一旦它们运行结束,应该调用哪些函数。
简单说,deferred对象就是jQuery的回调函数解决方案。
它解决了如何处理耗时操作的问题,对那些操作提供了更好的控制,以及统一的编程接口。
ajax操作的链式写法
jQuery 1.5 的变化:
- 无法改变 JS 异步和单线程的本质
- 只能从写法杜绝 callback 这种形式
- 语法糖,解耦代码
- 开放封闭原则:对扩展开放,对修改封闭。
先来对比一下,jQuery 1.5 前后的写法。
jQuery 1.5 前的 ajax 操作的传统写法:
1 | var ajax = $.ajax({ |
可以看到,这种写法返回的是 XHR 对象,你没法进行链式操作;如果高于1.5.0版本,返回的是deferred对象,可以进行链式操作。
jQuery 1.5 后的写法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19var ajax = $.ajax('./data.json')
ajax.done(function () {
console.log('success a');
}).fail(function () {
console.log('fail1');
}).done(function () {
console.log('success b');
}).fail(function () {
console.log('fail2');
}).done(function () {
console.log('success c');
}).fail(function () {
console.log('fail3');
})
console.log(ajax); // 返回一个 deferred 对象
// success a
// success b
// success c
采用链式写法以后,代码的可读性大大提高。而且可以添加多个回调函数。
指定同一操作的多个回调函数
如果是前一种写法,要添加一个成功的回调函数就只能在原有的代码上叠加:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 前一种写法:
var ajax = $.ajax({
url: './data.json',
success: function () {
console.log('success1');
console.log('success2');
console.log('success3');
// 添加一个成功的回调:
console.log('success4');
},
error: function () {
console.log('error');
}
})
console.log(ajax); // 返回一个 XHR 对象
// success1
// success2
// success3
// success4
这种写法不符合开放封闭原则,对扩展封闭,对修改开放。而且代码耦合度高。
但如果是后一种写法的话,要添加一个成功的回调函数只需直接把它加在后面即可:
1 | var ajax = $.ajax('./data.json') |
可以看到,使用这种写法,可以添加任意多个回调函数,并按照添加顺序执行。
1 | var ajax = $.ajax('./data.json') |
为多个操作指定回调函数
deferred对象的另一大好处,就是它允许你为多个事件指定一个回调函数。1
2
3
4
5$.when($.ajax("test1.html"), $.ajax("test2.html"))
.done(function(){ alert("哈哈,成功了!"); })
.fail(function(){ alert("出错啦!"); });
先执行两个操作$.ajax(“test1.html”)和$.ajax(“test2.html”),如果都成功了,就运行done()指定的回调函数;如果有一个失败或都失败了,就执行fail()指定的回调函数。
普通操作的回调函数接口
deferred对象的最大优点,就是它把这一套回调函数接口,从ajax操作扩展到了所有操作。也就是说,任何一个操作—-不管是ajax操作还是本地操作,也不管是异步操作还是同步操作—-都可以使用deferred对象的各种方法,指定回调函数。
假定有一个很耗时的操作wait:
1 | var wait = function(){ |
现在,我们接到一个新需求:为它制定一个回调函数。
我们可以直接在要执行的函数后面叠加:
1 | var wait = function(){ |
但是这样跟传统写法一样,不符合开放封闭原则,而且代码耦合度高,不利于维护。
下面是改进后的代码:
1 | function waitHandle() { |
$.when()的参数只能是deferred对象。
jQuery规定,deferred对象有三种执行状态—-未完成,已完成和已失败。如果执行状态是”已完成”(resolved),deferred对象立刻调用done()方法指定的回调函数;如果执行状态是”已失败”,调用fail()方法指定的回调函数;如果执行状态是”未完成”,则继续等待。
前面部分的ajax操作时,deferred对象会根据返回结果,自动改变自身的执行状态;但是,在wait()函数中,这个执行状态必须由程序员手动指定。dtd.resolve()的意思是,将dtd对象的执行状态从”未完成”改为”已完成”,从而触发done()方法。
还存在一个deferred.reject()方法,作用是将dtd对象的执行状态从”未完成”改为”已失败”,从而触发fail()方法。
上面这种写法,有一个问题:deferred对象的执行状态可以从外部改变:
1 | var w = waitHandle () |
deferred.promise()方法
为了避免以上这种情况,jQuery提供了deferred.promise()方法。它的作用是,在原来的deferred对象上返回另一个deferred对象,后者只开放与改变执行状态无关的方法(比如done()方法和fail()方法),屏蔽与改变执行状态有关的方法(比如resolve()方法和reject()方法),从而使得执行状态不能被改变。
1 | function waitHandle() { |
$.Deferred()
另一种防止执行状态被外部改变的方法,是使用deferred对象的建构函数$.Deferred()。
这时,waitHandle函数还是保持不变,我们直接把它传入$.Deferred():
1 | $.Deferred(waitHandle) |
jQuery规定,$.Deferred()可以接受一个函数名(注意,是函数名)作为参数,$.Deferred()所生成的deferred对象将作为这个函数的默认参数。
部署deferred接口
除了上面两种方法以外,我们还可以直接在wait对象上部署deferred接口。
1 | var dtd = $.Deferred() |
这里的关键是dtd.promise(wait)这一行,它的作用就是在wait对象上部署Deferred接口。正是因为有了这一行,后面才能直接在wait上面调用done()和fail()。