jQuery源码之Callbacks

什么是jQuery.Callbacks

首先来考虑一个问题,如果要让一些函数按照先后顺序执行该怎样写?

没错,用队列。实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function doByIndex(list,callback){
var task;
while(list.length>0){
task=list.shift();
task();
}
callback();
}
doByIndex([function(){
console.log('1');
},function(){
console.log('2')
},function(){
console.log('3');
}],function(){
console.log('done');
});

控制台会依次打印1 2 3 done;这种方式要判断函数的长度,每次都要去取出函数再去执行而且添加函数进去也不太方便,再来看看用jQuery.Callbacks实现:

1
2
3
4
5
6
7
8
var callback=$.Callbacks();
callback.add(function(){
alert('a');
});
callback.add(function(){
alert('b');
});
callback.fire();

使用起来很方便有木有,添加函数直接add,执行就fire,那jQuery.Callbacks究竟是什么呢,通过上面这个小demo应该可以发现,jQuery.Callbacks就是一个回调函数列表对象,封装了一些方法来管理回调函数队列

深入解析jQuery.Callbacks

既然jQuery.Callbacks是一个封装了很多方法的对象,那我们就来看看它内部是如何实现这些方法的。

事例说明

首先来说一说创建一个callback的列表有哪些可选的参数

  • once:确保callback回调列表只被执行一次(像一个递延deferred)
  • memory:保持上一个值,将添加到列表后最新的函数立即执行(像一个递延deferred)
  • unique:确保一个callback只能被添加一次(列表中无重复)
  • stopOnFalse:当一个callback返回false时,调用立即中断。

下面通过几个小demo分别看看这几个参数的意思。

1.关于once

1
2
3
4
5
6
7
8
9
var callbacks=$.Callbacks('once');
function fn1(n){
console.log('fn1 say'+n);
}
callbacks.add(fn1);
callbacks.fire('1');
callbacks.add(fn1);
callbacks.fire('2');

执行结果是只输出:fn1 say 1;因为once的存在,函数只会执行一次。所以fire(‘2’)不会执行。

2.关于memory

1
2
3
4
5
6
7
8
9
10
11
12
var callbacks=$.Callbacks('memory');
function fn1(n){
console.log('fn1 say'+n);
}
function fn2(n){
console.log('fn2 say'+n);
}
callbacks.add(fn1);
callbacks.fire('2');
callbacks.add(fn2);
//callbacks.fire('3');

执行结果为:fn1 say2 , fn2 say2;会输出fn2 say2的原因就是因为memory的存在,保持了上一个value值2,即使这里没有fire,也会立即调用执行fn2.

注:当前模式是’memory’模式,并且fire()过了,这时候,add()方法还能用,fire()就失效了
来通过另一个例子加深印象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var callbacks=$.Callbacks('once memory');
callbacks.add( fn1 );
callbacks.fire( "1" );
callbacks.add( fn1 );
callbacks.fire( "2" );
callbacks.add( fn1 );
callbacks.fire( "3" );
callbacks.add( fn1 );
callbacks.fire( "4" );
function fn1(i){
console.log('fn1 say:'+i);
}

执行结果就是四个1,原因就是,在第一次fire的时候清空了list,并且有memory的存在,第2,3,4次的add会在[this,’1’]上调用;第2,3,4次fire的时候,list实际上是空的,并且stack为false,所以第2,3,4次fire()不能执行。

3.关于unique

1
2
3
4
5
6
7
var callbacks=$.Callbacks('unique');
function fn1(n){
console.log('fn1 say'+n);
}
callbacks.add(fn1);
callbacks.add(fn1);
callbacks.fire('1');

执行结果就是fn1 say1;unique这个应该好理解,只添加一次嘛

4.关于stopOnFalse

1
2
3
4
5
6
7
8
9
10
11
var callbacks=$.Callbacks('stopOnFalse');
function fn1(n){
console.log('fn1 say'+n);
return false;
}
function fn2(n){
console.log('fn2 say'+n);
}
callbacks.add(fn1);
callbacks.add(fn2);
callbacks.fire('1');

执行结果是fn1 say1,因为当fire(‘1’)执行到fn1时已经中断调用了不会执行fn2.同样的如果是如下情况:

1
2
3
4
callbacks.add(fn1);
callbacks.fire('1');
callbacks.add(fn2);
callbacks.fire('2');

执行结果也就成了fn1 say1,fn1 say2了。执行完fire(‘1’)后不会执行add(fn2)了。所以我理解这里的interrupt callings指的是中断继续向callback的list中添加函数。

函数原型介绍

jQuery.Callbacks是在jQuery内部使用,如为.ajax$.Deferred等组件提供基础功能的函数,在jQuery引入了Deferred对象(异步列队)之后,jQuery内部基本所有有异步的代码都被promise所转化成同步代码执行

jQuery.Callbaks的内部实现

如下:

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
35
36
37
38
39
40
41
42
43
44
jQuery.Callbacks = function( options ) {
//将字符串格式选项转换成对象格式,在转换时会优先检测缓存
options = typeof options === "string" ?
( optionsCache[ options ] || createOptions( options ) ) :
jQuery.extend( {}, options );
var memory,
fired, //Callbacks列表是否已经执行
firing, // 标记当前Callbacks列表是否正在运行
firingStart, //Callbacks列表运行时,开始循环的第一个回调函数
firingLength, //Callbacks运行时,循环结束位置
firingIndex, //当前正在运行的Callbacks的索引(下标)
list = [], // 实际的回调函数列表
stack = !options.once && [],
// 只有在选项没有设置为once时,stack才存在
// stack用来存储参数信息(此时函数列表已经处于firing状态,必须将其他地方调用fire时的参数存储,之后再至此执行fire)
fire = function( data ) { },
// 实际的callback对象
self = {
// 回调列表中添加一个回调函数或回调函数的集合
add: function() { },
// 从回调列表中的删除一个回调函数或回调函数集合
remove: function() {},
// 返回是否列表中已经拥有一个相同的回调函数
has: function( fn ) { },
empty: function() { }, // 从列表中删除所有的回调函数
disable: function() {}, // 禁用列表中的回调函数
disabled: function() {}, // 确定列表是否已被禁用
lock: function() {}, // 锁定当前状态的回调函数列表
locked: function() {}, // 确定回调函数列表是否已被锁定
// 访问给定的上下文和参数列表中的所有回调函数
fireWith: function( context, args ) {},
fire: function() {}, // 用给定的参数调用所有的回调函数
fired: function() {} // 判断回调函数是否被已经被调用了至少一次
};
return self;
};

add()的内部实现

如下:

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
add: function() {
if ( list ) {
// 首先记录下当前list的长度
var start = list.length;
(function add( args ) {
jQuery.each( args, function( _, arg ) {
var type = jQuery.type( arg );
//如果传进来的是函数
//并且没有设置'unipue',直接push进数组
//如果有设置'unique',则判断list中是否存在该函数,不存在则push
if ( type === "function" ) {
//类似于写法if (A) else if (B)
if ( !options.unique || !self.has( arg ) ) {
list.push( arg );
}
} else if ( arg && arg.length && type !== "string" ) {
// 如果传进来的是一个数组则递归调用add实现函数添加
add( arg );
}
});
})( arguments );
// 当回调函数正在执行时,则修改firingLength,确保当前添加的回调函数能够被执行
if ( firing ) {
firingLength = list.length;
} else if ( memory ) {
// 如果不是firing'状态且设置memory,则立即执行刚刚添加的函数;memory 在这里是上一次 fire 的 [context, args]
firingStart = start;
fire( memory );
}
}
return this;
},

注释写在了上述代码中,应该还是比较好理解的,有一点要说一下,调用add()方法,是判断的是list,list不为undefined,所以可以添加成功。

fire()的内部实现

从上面的叙述中可以知道self是真正的Callbacks对象,self.fire和self.fireWith是对外提供的方法,而实际上实现fire功能的是Callbacks的内部实现方法,所以调用顺序是这样的,self.fire–>self.fileWith–>fire
如下是fire的内部实现:

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
fire = function( data ) {
memory = options.memory && data;//如果参数memory为true,则记录下data
fired = true; //标记运行过的回调函数
firingIndex = firingStart || 0; //正在执行的回调函数的下标
firingStart = 0;
firingLength = list.length;
firing = true; //标记正在运行的回调函数
//遍历list,执行所有回调;data[ 0 ]是函数执行的上下文,也就是平时的this
for ( ; list && firingIndex < firingLength; firingIndex++ ) {
if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) {
memory = false; // 阻止未来可能由于add所产生的回调
break;// 由于参数options设置了stopOnFalse,当有回调函数运行结果为false时,退出循环
}
}
firing = false; // 标记结束运行回调
if ( list ) {
if ( stack ) {
// stack不为空,即stack中存有参数信息,
// 当firing在运行时,通过add添加的Callbacks都将保存到stack中
if ( stack.length ) {
fire( stack.shift() );
}
} else if ( memory ) {
// "once memory" 或者 "memory" 情况下 lock 过。
list = [];
} else {
self.disable(); // 阻止回调列表中的回调
}
}
},

调用fire()方法,看的是stack,如果不为undefined,就能执行

要注意的是这句memory = options.memory && data;执行完这句后memory并不是通常认识上的Boolean值,
在javascript意思是:

>1 当options.memory存在时,该语句就相当于一条赋值语句memory = data;;
>2 当options.memory不存在时,memory值为false

remove()

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
remove: function() {
if ( list ) {
jQuery.each( arguments, function( _, arg ) {
var index;
//为什么要循环呢?因为一个回调可以被多次添加到队列
while( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {
//从队列里删除
list.splice( index, 1 );
// 当前在触发中
if ( firing ) {
//移除的元素如果是之前fire的,把firingLength减一
if ( index <= firingLength ) {
firingLength--;
}
//如果移除的元素比当前在firing的索引要小,所以firingIndex也要退一步回前边一格
if ( index <= firingIndex ) {
firingIndex--;
}
}
}
});
}
return this;
},

其他:

has()就是通过内部调用isArray()来判断是否在队列里面;empty()就是清空队列;disable()就是禁用,清空了所有的队列,栈,无法进行任何操作了;disabled就是通过队列是否存在来判断是否被禁用