jQuery的deferred对象

Author Avatar
Klein 8月 18, 2018

什么是deferred对象

web 开发中常遇到耗时很长的 javascript 操作,既有异步的操作(比如ajax读取服务器数据),也有同步的操作(比如遍历一个大型数组),它们都不是立即能得到结果的。

通常的做法是,为它们指定回调函数(callback)。即事先规定,一旦它们运行结束,应该调用哪些函数。

简单说,deferred对象就是jQuery的回调函数解决方案。

它解决了如何处理耗时操作的问题,对那些操作提供了更好的控制,以及统一的编程接口。

ajax操作的链式写法

jQuery 1.5 的变化:

  • 无法改变 JS 异步和单线程的本质
  • 只能从写法杜绝 callback 这种形式
  • 语法糖,解耦代码
  • 开放封闭原则:对扩展开放,对修改封闭。

先来对比一下,jQuery 1.5 前后的写法。

jQuery 1.5 前的 ajax 操作的传统写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var ajax = $.ajax({
url: './data.json',
success: function () {
console.log('success1');
console.log('success2');
console.log('success3');
},
error: function () {
console.log('error');
}
})
console.log(ajax); // 返回一个 XHR 对象

// success1
// success2
// success3

可以看到,这种写法返回的是 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
19
var 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var 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');
})
// 添加的回调:
.done(function () {
console.log('success d');
})
console.log(ajax); // 返回一个 deferred 对象

// success a
// success b
// success c
// success d

可以看到,使用这种写法,可以添加任意多个回调函数,并按照添加顺序执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var ajax = $.ajax('./data.json')
ajax.then(function () {
console.log('success 100')
}, function () {
console.log('fail 100')
}).then(function () {
console.log('success 200')
}, function () {
console.log('fail 200')
}).then(function () {
console.log('success 300')
}, function () {
conso3le.log('fail 100')
})

为多个操作指定回调函数

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
2
3
4
5
6
7
var wait = function(){
var tasks = function(){
alert("执行完毕!");
};
setTimeout(tasks,5000);
};
wait()

现在,我们接到一个新需求:为它制定一个回调函数。

我们可以直接在要执行的函数后面叠加:

1
2
3
4
5
6
7
8
var wait = function(){
var tasks = function(){
alert("执行完毕!");
// 很多很多的代码
};
setTimeout(tasks,5000);
};
wait()

但是这样跟传统写法一样,不符合开放封闭原则,而且代码耦合度高,不利于维护。

下面是改进后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function waitHandle() {
// 定义 Deferred 实例
var dtd = $.Deferred()
var wait = function (dtd) {
var task = function () {
console.log('执行完成')
// 成功
dtd.resolve()
// 失败
// dtd.reject()
}
setTimeout(task, 1000)
// wait 返回
return dtd()
}
// 最终返回
return wait(dtd)
}

var w = waitHandle ()
$.when(w)
.then(function () {
console.log('ok 1')
}, function () {
console.log('err 1');
})
w.then(function () {
console.log('ok 2')
}, function () {
console.log('err 2');
})

// ok 1
// ok 2

$.when()的参数只能是deferred对象。

jQuery规定,deferred对象有三种执行状态—-未完成,已完成和已失败。如果执行状态是”已完成”(resolved),deferred对象立刻调用done()方法指定的回调函数;如果执行状态是”已失败”,调用fail()方法指定的回调函数;如果执行状态是”未完成”,则继续等待。

前面部分的ajax操作时,deferred对象会根据返回结果,自动改变自身的执行状态;但是,在wait()函数中,这个执行状态必须由程序员手动指定。dtd.resolve()的意思是,将dtd对象的执行状态从”未完成”改为”已完成”,从而触发done()方法。

还存在一个deferred.reject()方法,作用是将dtd对象的执行状态从”未完成”改为”已失败”,从而触发fail()方法。

上面这种写法,有一个问题:deferred对象的执行状态可以从外部改变:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var w = waitHandle ()
// 从外部改变 deferred对象
w.reject
$.when(w)
.then(function () {
console.log('ok 1')
}, function () {
console.log('err 1');
})
.then(function () {
console.log('ok 2')
}, function () {
console.log('err 2');
})

// err 1
// err 2

deferred.promise()方法

为了避免以上这种情况,jQuery提供了deferred.promise()方法。它的作用是,在原来的deferred对象上返回另一个deferred对象,后者只开放与改变执行状态无关的方法(比如done()方法和fail()方法),屏蔽与改变执行状态有关的方法(比如resolve()方法和reject()方法),从而使得执行状态不能被改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function waitHandle() {
var dtd = $.Deferred()
var wait = function (dtd) {
var task = function () {
console.log('执行完成')
dtd.resolve()
}
setTimeout(task, 1000)
// 注意,这里返回的是 promise 而不是直接返回 deferred 对象
return dtd.promise()
}
return wait(dtd)
}

var w = waitHandle ()
// 如果这样写会报错
w.reject
$.wait(w)
.then(function () {
console.log('ok 1')
}, function () {
console.log('err 1');
})
.then(function () {
console.log('ok 2')
}, function () {
console.log('err 2');
})

$.Deferred()

另一种防止执行状态被外部改变的方法,是使用deferred对象的建构函数$.Deferred()。

这时,waitHandle函数还是保持不变,我们直接把它传入$.Deferred():

1
2
3
4
5
6
7
8
9
10
11
$.Deferred(waitHandle)
.then(function () {
console.log('ok 1')
}, function () {
console.log('err 1');
})
.then(function () {
console.log('ok 2')
}, function () {
console.log('err 2');
})

jQuery规定,$.Deferred()可以接受一个函数名(注意,是函数名)作为参数,$.Deferred()所生成的deferred对象将作为这个函数的默认参数。

部署deferred接口

除了上面两种方法以外,我们还可以直接在wait对象上部署deferred接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var dtd = $.Deferred()
function waitHandle(dtd) {
var wait = function (dtd) {
var task = function () {
console.log('执行完成')
dtd.resolve()
}
setTimeout(task, 1000)
}
}

dtd.promise(waitHandle);

waitHandle
.then(function () {
console.log('ok 1')
}, function () {
console.log('err 1');
})
.then(function () {
console.log('ok 2')
}, function () {
console.log('err 2');
})

这里的关键是dtd.promise(wait)这一行,它的作用就是在wait对象上部署Deferred接口。正是因为有了这一行,后面才能直接在wait上面调用done()和fail()。

参考资料

jQuery的deferred对象详解