以下内容纯属自己在学习期间的心得体会记录,也希望对人有帮助,欢迎指出不足。
jQuery的Deferred模块用来解决异步编程的回调函数注册问题。
对于前端来说,最熟悉的异步操作自然是ajax了。我们来看看在JQuery1.5版本以前的写法是怎么样的。
$.ajax({
url: "ceshi.html",
success:function(){
alert("成功!");
},
error:function(){
alert("出错!");
}
});
这就注册了两个函数,异步网络请求执行成功和失败的回调函数。而这两个函数最终会在哪里执行呢?
在jQuery代码里面,用原生JS来写的话,这两个函数的执行地点也很简单。简单示例如下(jQuery里面自然比这里写的要复杂):
function ajax(setting){
var xhr = newXMLHttpRequest();
xhr.onreadystatechange= function(){
if(xhr.readyState=== 4){
if(xhr.status=== 200){
setting.success(xhr.responseText);
}else{
setting.error(xhr);
}
}
};
xhr.open("GET", setting.url,true);
xhr.send(null);
}
可以看得出,注册的两个回调函数,他们最终会在满足某个条件的状况下选择一个被执行。在这两个注册函数上面,还有一层事件注册函数。所以呢,这俩注册的回调函数被执行的时间是不确定的,需要由事件何时发生来决定。
但是,我们在这里可以这样子来理解这两个函数,我们在调用最外面的$.ajax()函数时,就会在将来的某个时刻,定会执行的代码中,放了一个鱼钩加饵进去(也就是被注册的俩回调函数,相当于一个钩子。),等着鱼来吃。这说起来就和钓鱼一样,鱼什么时候会上钩,就只有等待,等着事件发生,也就是等着鱼来上钩。
只要鱼吃掉了鱼饵,也就是onreadystatechange这个事件被唤醒了,那么我们注册的函数肯定就会知道从而被执行。至于哪个函数会被执行,就要由onreadystatechange的事件函数来决定。
这样子说来,异步编程就是放鱼钩等鱼来吃,鱼上钩了就会按既定程序来拉鱼竿的模式了。说专业话就是注册回调函数,等待将来某个时刻被执行。而对于异步编程来说,这样子放鱼钩的方式几乎是不会变的,但是注册回调函数的方式却是能变的。这就是deferred所做的改变。
我们来看看 jQuery 1.5版本后,采用deferred模块来注册回调函数的方式。
$.ajax("ceshi.html")
.done(function(){ alert("成功") })
.fail(function(){ alert("出错")});
唯一的变化就是不再由一个函数$.ajax来传入所有的设置和回调函数。而是分为三步走。先传入请求的网页地址,再注册网络异步请求执行成功的回调函数,最后注册网络异步请求执行失败的回调函数。而成功和失败的回调函数注册是可以互换位置的,失败在前,成功在后也行,注册的顺序不限制。(到了这里就有点想吐槽的了,最后再说吐槽点。)
这和以前的方式相比,大体来看差别不是很大,只不过将两个回调函数的注册交给了另外两个接口函数而已。不再是一步到位,而是三步到位。
这有什么好处???
我所知道的有三点好处。
一、是可以让以前传入的一个统一的设置对象分隔开来,这种链式分步骤的写法让程序更容易阅读。
二、是可以注册多个回调函数。就像下面这样
$.ajax("test.html")
.done(function(){ alert("成功")} )
.fail(function(){ alert("出错")} )
.done(function(){ alert("第二次成功")} )
.fail(function(){alert( “第二次出错”)});
注册的多个回调函数会依注册顺序依次执行。
三、是可以为多个异步操作注册一个统一的回调函数,像下面这样。
$.when($.ajax("ceshi1.html"),$.ajax("ceshi2.html"))
.done(function(){ alert("成功") })
.fail(function(){alert("出错") });
在两个异步操作都成功后,执行成功的注册函数。只要其中有一个异步执行失败,就会执行失败的注册函数。when是deferred模块的一种接口。
其他的好处只有求高手回答了。
第一个好处其实并不是太大,而对于第二和第三来说,就甚为有意义了。以前老的写法也可以实现第二和第三种好处,但是,肯定没deferred的方式清晰明了和方便。
还有一点要注意,虽然都是以ajax来举例,但是deferred的应用对象可不只是AJAX。只要是异步的操作(比如HTML5中本地数据库的操作),需要注册回调函数,就都可以使用这种方式。
那么,新问题来了,怎么使用deferred呢?对于AJAX的操作,jQuery已经帮我们封装好了使用方式,所以对于上面的例子有可能看起来不是太明白。
想明白使用方式,我们要回到钓鱼这个话题上来。在上面的代码示例中,有一个原生JS使用AJAX的例子。在那里面我们下了两个钩子,用来主动调用了两个注册函数,也就是setting.success(xhr.responseText)和setting.error(xhr);。这是对于用以前老的方式来注册回调函数而言,就直接执行注册的回调函数。
而对于使用了deferred模块后,该怎么样来让注册的回调函数执行呢?也不复杂,增加了几个deferred的东西而已。如下所示。
function ajax(url){
// 新建一个deferred对象
var def =$.Deferred();
var xhr =new XMLHttpRequest();
xhr.onreadystatechange= function(){
if(xhr.readyState=== 4){
if(xhr.status=== 200){
// 改变deferred对象的状态为成功
def.resolve(xhr. responseText);
}else{
// 改变deferred对象的状态为失败
def.reject (xhr);
}
}
};
xhr.open("GET",url , true);
xhr.send(null);
//返回promise对象
return def.promise();
}
和上面原生JS的代码对比一下,会发现ajax函数首尾增加了两行代码,注册的回调函数执行的地方,更改了两行代码。这样子就把一个有异步操作的原生JS函数ajax,改成了用deferred模块来实现的函数。
然后来解释下这四行代码的作用。清楚明白后就可以把任意的有异步操作的函数改成用deferred来实现。从而就可以使用最上面的deferred方式(用done、fail、 then、progress、always等函数来注册)来注册回调函数,也就拥有了上面所说的三点好处。而这四行代码可能是deferred最让人难理解的地方了。
先来看看中间被更改的两行代码。
这里没有直接调用注册的回调函数,也就是没有直接拉鱼竿。为什么啊?因为没有函数的引用让我们来调用啊,ajax这时候只传入了一个url参数,能调用到个屁啊。那我们调用的是什么啊?
我们主动调用了Deferred对象的两个方法接口resolve和reject。翻译过来而言,和成功失败是差不多的道理。那也就很好理解了。这鱼钩和饵(注册的回调函数)在这里变了,不是我们主动投下去的,而是有一个代理帮我们投下去的(done、fail等接口注册回调函数),还提供帮助,给了我们拉鱼竿的权力(执行已注册的回调函数,也就是def.resolve()和def. reject ())。
以前的老方式是两步走,注册回调函数,执行回调函数。现在用deferred也是如此,只不过变成了注册是用done、fail这个接口,而执行变成了用resolve和reject这些个接口实现。
上面说的代理就是deferred对象了。这和订阅/发布者模式何其相似啊,简直是一模一样的。投饵就是订阅,拉鱼竿就是发布。订阅就是注册回调函数,发布就是执行已注册的回调函数。
以前注册回调函数是直接传进去,现在呢,先不传,而是缓一步再传。注册就要用到代理deferred给我们提供的接口——done、fail、then、always和progress等。
说是代理感觉也不太好理解。可以把deferred看成第三方专门提供帮助的人,是你的女仆如何?
在这里,你有命令女仆做事的权力。你可以在看见鱼上钩后,命令女仆拉鱼竿(resolve)。或者发觉鱼跑了,让女仆换鱼饵(reject)。这个看见是在注册的事件函数里的,所以这个看见是不可测的异步操作。
当调用了resolve命令,也就是这样def.resolve()后,注册的所有成功时执行的回调函数都会依次执行。而调用reject命令,也就是这样def. reject ()后,注册的所有失败时执行的回调函数都会依次执行。
当然,同时也可以传递参数给所有回调函数,只需要在命令函数里加上参数即可。比如这样。def.resolve(‘嗨,各位大家好’)。那么done(function(a){alert(a)}),注册的函数即可从a参数接收到。最终弹出窗口显示“嗨,各位大家好”。传递的参数个数不限。
deferred对象有三种执行状态----未完成,已完成和已失败。从未完成到已完成或已失败的状态间转换就靠这两个命令了。
其实还有其他的命令(notify、notifyWith、resolveWith、rejectWith),但在这里暂时知道这两个就行了,其他的命令在理解deferred后,自己看文档就能看懂。
中间两行代码说到底也是在执行注册的回调函数,只不过借了别人的嘴来命令它们执行而已。
那么首行创建deferred对象的代码就好理解了。每次异步操作,都需要一个deferred来帮助我们。所以每个有异步操作的函数里,我们都要新建一个deferred。
好了,现在我们来看看最下面的函数返回值——返回一个promise对象,从deferred身上生成的。这个promise有什么用哦?
上面说到了,注册回调函数是缓了一步再注册的。但是在哪里去注册啊,注册给谁啊?这就是promise的作用。也就是用promise对象提供的接口去注册回调函数。所以改成deferred模式的ajax函数可以如下这样写了。
ajax(‘ceshi.html’).done().fail();
ajax函数返回了promise对象,而promise对象提供了done、fail这些注册回调函数的接口方法。而done和fail这些接口在调用后又返回自身对象,所以就可以这样连缀的写下来,到达了缓一步注册的目的,也让异步看起来变成了同步。
“异步变同步”这点对于Node而言简直就是大善!!让大量的回调看起来变成了同步执行。比如上面的例子,阅读代码时看起来就像是,先ajax去请求网页,然后成功就怎么样,失败又怎么怎么样的。
但是像事件注册这样的写法,一般都是先注册,再调用。而deferred是先调用后,再来注册回调函数,所以有些人在这里会患迷糊。(所以也有个槽点了)
疑问来了,为什么不直接把接口放在deferred身上来返回呢?这又是deferred,又是promise的,不嫌麻烦啊!而这样做当然是有原因的。
看下面这个直接返回deferred对象的ajax函数,事实上deferred对象上是有注册回调函数的接口done、fail、then、always和progress等的。所以确实可以直接返回。那么来看看这样子返回有什么不好吧。
function ajax(url){
// 新建一个deferred对象
var def =$.Deferred();
var xhr =new XMLHttpRequest();
xhr.onreadystatechange= function(){
if(xhr.readyState=== 4){
if(xhr.status=== 200){
// 改变deferred对象的状态为成功
def.resolve(xhr.responseText);
}else{
//改变deferred对象的状态为失败
def.reject (xhr);
}
}
};
xhr.open("GET", url , true);
xhr.send(null);
//返回deferred对象
return def;
}
var def = ajax(‘ceshi.html’)
.done(function(){alert(‘成功’)})
.fail(function(){alert(‘失败’)});
//注意这里!!我们在ajax外部来调用女仆定会听从的命令resolve
def.resolve ();
上面这段代码的执行结果是立即弹出窗口,内容是“成功”。为什么?因为我们把deferred的命令接口暴露出来了。我们在外面就可以命令这可怜的女仆做任何事。相当于执掌虎符的大将军把虎符主动送给别人,这军队自然归别人掌管了,就任人宰割。这样子自然非常不好。
所以我们不能返回有命令接口的deferred对象,而是只有注册回调函数接口的promise对象。这样子别人怎么也命令不了你自己的女仆了。对外开放的只有放饵的权力,鱼上钩了后,拉鱼竿就轮不到别人来做,只有自己能做,这多好的事情啊。
综上所述,如果自己有一个有异步操作的函数,要把它变为deferred模式就需要三步。
第一步需要有一个deferred对象,一般通过$.Deferred()生成。
第二步根据自己解决问题和业务的逻辑来确定调用哪个命令,一般是resolve ()和reject();
第三步,通过deferred对象生成的promise作为函数返回值。一般这样子写,return def.promise();
如此,我们便可以用deferred的方式来使用这个函数了。
有哪几种使用方式呢?也就是哪几种注册回调函数的方式。
通过done和fail接口是最常见的。还有then接口,他一次性接收两个注册函数,第一个参数是成功时的回调函数,第二个是失败后的回调函数。
ajax(‘ceshi.html’).then(function(){alert(‘成功’)},function(){alert(‘失败’)});
注意then接口在jQuery1.8版本后又有了新的变化,可以用来过滤改变,命令接口传入的参数值。参考官方文档http://api.jquery.com/deferred.then/。
还有progress、always 、jQuery.when()以及pipe,使用方法下次再说吧。
有异步操作的函数里通常需要自己生成一个deffered辅助对象,就相当于自己买个女娃娃当仆人了。
其实有一种方式可以不用自己生成,而是让$.Deferred方法从函数外面由参数传递进来。$.Deferred(ajax).done().fail()。如果采用这样的写法,说明ajax函数第一个参数就是一个deferred对象,直接用就行了,不用新建。但有个缺点,ajax函数这样子就会接收不到其他参数,而只有一个参数。更大的缺点是deferred对象从$.Deferred接口的返回值暴露出来了,所以不推荐这种做法。
说到这里,反正我自己心里对于使用deferred的方式是大概理解到了。也希望上面这一大段乱七八糟的文字能对人有帮助。
最后,来说说那个吐槽点。对于先执行再注册这种模式,一看就有点矛盾了。万一异步操作执行完毕后,注册却还没执行到,造成注册失败,那么不就做了无用功嘛!不过,在很大很大部分情况下,这种情况是很少很少发生的。因为从执行异步函数完毕到注册成功,只有短短那么几个链式写法的注册函数跳转而已。如果真遇到这情况,那么这异步操作也真没异步的必要了,直接同步不是更好?