上一篇博客分析了Callbacks实现原理,而在jQuery中,Ready函数的实现,异步队列以及队列模块都用到了Callbacks对象。jQuery.ready函数在前面已经做了整理,所以这篇博客主要是分析Deffered(异步队列)和jQuery中异步队列的应用。
异步队列
延迟对象(异步队列)是在回调对象的基础上实现的。这个延迟对象维护了三个列表:成功(done)回调函数列表,失败(fail)回调函数列表和进行中(progress)回调函数列表,之所以有三个回调函数列表,是因为延迟对象有三种状态,分别是:resolve(成功), reject(失败), notify(进行中)。
那么延迟对象是如何在运用回调对象模块来实现异步功能的?
看一个例子:
var dfd = $.Deferred(); //与回调对象类似,先定义一个延迟对象;
setTimeout( function(){ //当时间满足一秒时
dfd.resolve(); //相当于$.Callbacks().fire()方法,将加入到done(成功回调列表)中的函数执行,在这里将弹出有'Yes'的窗口
}, 1000);
dfd.done(function(){ //相当于$.Callbacks().add(function(){})方法
//不同的是,这里分别通过done和fail方法,将不同的回调函数添加到各自的回调列表中。
alert('Yes');
}).fail(function(){
alert('No'); //如果setTimeOut中,将dfd.resolve()换成dfd.reject(),将弹出有'No'的窗口
})
根据上面分析,我们可以得到这样一个对应关系:
下来我们看看源码中是如何实现的?
源码框架
jQuery.extend({
Deferred: function( func ){
//三个回调函数列表
doneList = $.Callbacks('once memory')
failList = $.Callbacks('once memory')
progressList = $.Callbacks('memory')
//初始状态
state = 'pending' //表示进行中
//三种状态和三个回调函数列表对应
list = {
resolve: doneList,
reject: failList,
notify: progressList
}
promise = {
done: doneList.add,
fail: failList.add,
progress: progressList.add,
state: function(){ },
then: function(){ }, //依次添加doneList,failList和progressList回调函数
always: function(){ }, //不管触发resolve函数还是reject函数,都会执行该方法添加的回调函数
pipe: function(){ },
promise: function(){ } //返回promise对象的一个副本或者扩展的延迟对象
}
//重新建一个promise的副本
deferred = promise.promise({});
//给deferred延迟对象添加三种状态函数
for(key in list)
{
deferred[key] = list[key].fire;
deferred[key + 'With'] = list[key].fireWith;
}
//执行deferred.resolve或者deferred.reject时,要改变初始的状态,并且一旦状态确定,便不会再更改,所以有下面的操作
deferred.done(function(){
state = 'resolved'
}, failList.disable, progressList.lock).fail(function(){
state = 'rejected'
}, doneList.disable, progressList.lock);
if( func ) { //Deferred也是支持参数的
func.call(deferred, deferred);
}
return deferred; //返回延迟对象
}
})
在源码中,我们可以清楚的看到,延迟对象是如何利用回调对象来实现回调函数的添加和执行的。除了这些,可以看到,在Deferred函数中,创建了一个promise对象,另外还又得到了一个deferred对象,这两个对象之间有什么区别和联系呢?
可以看到,deferred对象由promise对象扩展而来,但是又比promise对象多了三个函数,这三个函数都是用于改变延迟对象状态的。那么为什么要用两个延迟对象? 这是为了保证外部不能对延迟对象的状态进行改变。
举个例子:
//函数a返回一个延迟对象
function a(){
var dfd = $.Deferred();
setTimeout( function(){
dfd.resolve();
}, 1000);
return dfd;
}
//将a得到的延迟对象 赋值给一个新的延迟对象
var dfdNew = a().done(function(){
alert("Yes");
}).fail(function(){
alert('No');
});
//造成在函数外部,对延迟对象的状态进行了改变。
dfdNew.reject();
由于上面的a函数返回的是一个延迟对象deferred,所有包含有resolve,reject和notify方法,因此可以在函数外部去改变延迟对象状态,而此时我们是不希望外部对延迟对象的状态进行改变,因此这才利用到promise对象。看下面段代码:
function a(){
var dfd = $.Deferred();
setTimeout( function(){
dfd.resolve();
}, 1000);
return dfd.promise();
}
var dfdNew = a().done(function(){
alert("Yes");
}).fail(function(){
alert('No');
});
//此时得到dfdNew延迟对象,并没有resolve等改变状态的方法,因此下面的语句会出错。
dfdNew.reject();
这段代码就按照我们的意愿来执行了,外部并不能对延迟对象的状态进行更改,只能通过a函数里面的dfd.resolve来改变。
异步队列的应用
在jQuery中,主要有两个部分用到了异步队列(延迟对象)模块:
$.ajax
和 $().ready()
方法。
在ready函数理解这篇博客中,整理了jQuery的ready方法,但当时还没有学习关于回调对象,延迟对象的模块,所以有一部分没有深入,下来我们再来缕一缕ready函数。
主要看readyList.add(fn)
这部分代码,readyList是一个回调对象,$.Callbacks('once memory')
,这就相当于Deferred对象中,成功回调函数列表、失败回调函数列表,在jQuery.fn.ready函数中,将fn回调函数添加进回调列表中。
在页面元素加载出来之后,触发之前注册的事件处理函数,这个事件处理函数中会去调jQuery.ready()
方法,在这个方法中调readyList.fireWith()
方法,从而实现回调函数的执行。
ajax
模块暂时还没有看,所以后期整理时再回过头来分析对于Deferred延迟对象的应用。
when方法
jQuery扩展了一个when
方法,这个方法相当于是对Deferred
对象的一个延伸,相当于对多个延迟对象组合起来进行处理。看下面一个例子:
function a(){
var d = $.Deferred();
d.resolve();
return d;
}
function b(){
var d = $.Deferred();
d.resolve();
return d;
}
$.when(a(), b()).done(function(){
//当a()和b()同时返回一个resolve状态的延迟对象时,才会触发doneList回调列表中的回调函数
alert("Yes");
}).fail(function(){
//当a()或者b()任意一个返回reject状态的延迟对象,就会触发failList列表中的回调函数
alert("No");
})
//而当参数不是Deferred对象或者无参数时,始终触发doneList里面的回调函数;并且可以通过arguments类数组访问到传的参数。
$.when(123, 8945).done(function(){
alert("成功");
}).fail(function(){
alert("失败");
})
有上面分析和测试,可以想到,$.when()
和$.when(123, 456)
是相同的处理;只有在参数为延迟对象时,才会判断这些延迟对象的状态,从而决定$.when
后面添加的回调方法执行哪一个。
下来根据源码缕一遍:
function( firstParam ) {
//首先将参数转换成数组,调用[].slice方法
var args = sliceDeferred.call( arguments, 0 ),
i = 0,
length = args.length, //得到参数的个数
pValues = new Array( length ),
count = length,
pCount = length,
//根据参数个数,以及参数类型,设置deferred对象
//当无参数或者多于一个参数时,deferred = $.Deferred()对象;
//当且仅当参数是一个延迟对象类型时,deferred = 这个延迟对象
deferred = length <= 1 && firstParam && jQuery.isFunction( firstParam.promise ) ? firstParam : jQuery.Deferred(),
//deferred对象的一份拷贝,供外界使用,所以是利用promise方法得到,没有resolve(),reject()以及notify()方法,以为外界不允许对状态再进行改变
promise = deferred.promise();
//下面这两个函数先不看
function resolveFunc( i ) {
}
function progressFunc( i ) {
}
//下面利用参数个数分情况处理
if ( length > 1 ) {
//多个参数时,又分当前参数是否为延迟对象
for ( ; i < length; i++ ) {
//是延迟对象,分别向doneList列表,failList列表和progressList列表中添加回调。
//因为是延迟对象,即有'memory'标志的回调对象,如果之前延迟对象的状态已经确定,此时添加回调函数之后会立即执行回调函数。
//并且可以看到,添加到failList中的回调函数是deferred.reject,也就是说只要参数中有一个延迟对象的状态是reject,都会触发最终deferred对象的reject。
//而向doneList和progressList中添加的回调函数分别是resolveFunc和progressFunc,这两个函数后面再分析。
if ( args[ i ] && args[ i ].promise && jQuery.isFunction( args[ i ].promise ) ) {
args[ i ].promise().then( resolveFunc(i), deferred.reject, progressFunc(i) );
} else {
//如果参数不是延迟对象,count减1.
//这里说明一下count的含义。when整体的实现思路,是用一个计数器来记录是否所有的延迟对象都resolve或者都notify了。
//初始值为所有的参数个数,当参数不是延迟对象时,减一,当参数是延迟对象,且一直resolve或者一直notify,也减一。直到减为0时,触发deferred.resolve或者deferred.notify。
--count;
}
}
if ( !count ) {
//count减为0,触发deferred.resolve().
deferred.resolveWith( deferred, args );
}
} else if ( deferred !== firstParam ) {
//无参数时,触发deferred.resolveWith()
//一个参数时,且参数不为延迟对象时,同样触发deferred.resolveWith().
deferred.resolveWith( deferred, length ? [ firstParam ] : [] );
}
//一个参数,且参数为延迟对象时,deferred = 这个延迟对象。
//最后返回deferred.promise对象,也就是说,这个延迟对象的状态是什么,就去触发$.when后面添加的回调函数即可。类似于一个延迟对象的处理。
return promise;
}
下面来说resolveFunc
和 progressFunc
这两个方法:
//这两个方法分别是添加进doneList和 progressList回调列表中的回调函数
function resolveFunc( i ) {
//返回一个函数,这个函数每一次对计数器count减一,并且判断在count为0时,直接触发deferred.resolve。
return function( value ) {
args[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value;
if ( !( --count ) ) {
deferred.resolveWith( deferred, args );
}
};
}
//progressFunc与上面的函数是类似的,不同的是,这是对于progressList添加的回调函数
//更重要的不同点是: 只要有一个延迟对象的状态为notify,就都会触发deferred.notify。
function progressFunc( i ) {
return function( value ) {
pValues[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value;
deferred.notifyWith( promise, pValues );
};
}
到这里,关于回调对象,延迟对象以及延迟对象的扩展when都已经分析完了。下来为了加深对于这两个对象的理解和运用,后面可能会从jQuery
功能模块的ajax
方法来分析,期间对于回调对象和延迟对象的运用还会重点分析以加深理解。