jQuery源码学习之Callbacks
jQuery的ajax
、deferred
通过回调实现异步,其实现核心是Callbacks
。
使用方法
使用首先要先新建一个实例对象。创建时可以传入参数flags
,表示对回调对象的限制,可选值如下表示。
-
stopOnFalse
:回调函数队列中的函数返回false
时停止触发 -
once
:回调函数队列只能被触发一次 -
memory
:记录上一次触发队列传入的值,新添加到队列中的函数使用记录值作为参数,并立即执行。 -
unique
:函数队列中函数都是唯一的
var cb = $.Callbacks('memory');
cb.add(function(val){
console.log('1: ' + val)
})
cb.fire('callback')
cb.add(function(val){
console.log('2: ' + val)
})
// console输出
1: callback
2: callback
Callbacks
提供了一系列实例方法来操作队列和查看回调对象的状态。
-
add
: 添加函数到回调队列中,可以是函数或者函数数组 -
remove
: 从回调队列中删除指定函数 -
has
: 判断回调队列里是否存在某个函数 -
empty
: 清空回调队列 -
disable
: 禁止添加函数和触发队列,清空回调队列和上一个传入的值 -
disabled
: 判断回调对象是否被禁用 -
lock
: 禁用fire
,若memory非空则同时add无效 -
locked
: 判断是否调用了lock
-
fireWith
: 传入context
和参数,触发队列 -
fire
: 传入参数触发对象,context
是回调对象
源码解析
$.Callback()
方法内部定义了多个局部变量和方法,用于记录回调对象的状态和函数队列等,返回self
,在self
实现了上述回调对象的方法,用户只能通过self
提供的方法来更改回调对象。这样的好处是保证除了self
之外,没有其他修改回调对象的状态和队列的途径。
其中,firingIndex
为当前触发函数在队列中的索引,list
是回调函数队列,memory
记录上次触发的参数,当回调对象实例化时传入memory
时会用到,queue
保存各个callback执行时的context和传入的参数。self.fire(args)
实际是self.fireWith(this,args)
,self.fireWith
内部则调用了在Callbacks
定义的局部函数fire
。
...
// 以下变量和函数 外部无法修改,只能通过self暴露的方法去修改和访问
var // Flag to know if list is currently firing
firing,
// Last fire value for non-forgettable lists
// 保存上一次触发callback的参数,调用add之后并用该参数触发
memory,
// Flag to know if list was already fired
fired,
// Flag to prevent firing
// locked==true fire无效 若memory非空则同时add无效
locked,
// Actual callback list
// callback函数数组
list = [],
// Queue of execution data for repeatable lists
// 保存各个callback执行时的context和传入的参数
queue = [],
// Index of currently firing callback (modified by add/remove as needed)
// 当前正触发callback的索引
firingIndex = -1,
// Fire callbacks
fire = function() {
...
},
// Actual Callbacks object
self = {
// Add a callback or a collection of callbacks to the list
add: function() {
...
},
...
// Call all callbacks with the given context and arguments
fireWith: function( context, args ) {
if ( !locked ) {
args = args || [];
args = [ context, args.slice ? args.slice() : args ]; // :前为args是数组,:后是string
queue.push( args );
if ( !firing ) {
fire();
}
}
return this;
},
// Call all the callbacks with the given arguments
fire: function() {
self.fireWith( this, arguments );
return this;
},
...
}
通过self.add
添加函数到回调队列中,代码如下。先判断是否memory
且非正在触发,如果是则将fireIndex
移动至回调队列的末尾,并保存memory
。接着使用立即执行函数表达式实现add函数,在该函数内遍历传入的参数,进行类型判断后决定是否添加到队列中,如果回调对象有unique
标志,则还要判断该函数在队列中是否已存在。如果回调对象有memory
标志,添加完毕之后还会触发fire
,执行新添加的函数。
add: function() {
if ( list ) {
// If we have memory from a past run, we should fire after adding
// 如果memory非空且非正在触发,在queue中保存memory的值,说明add后要执行fire
// 将firingIndex移至list末尾 下一次fire从新add进来的函数开始
if ( memory && !firing ) {
firingIndex = list.length - 1;
queue.push( memory );
}
( function add( args ) {
jQuery.each( args, function( _, arg ) {
// 传参方式为add(fn)或add(fn1,fn2)
if ( jQuery.isFunction( arg ) ) {
/**
* options.unique==false
* 或
* options.unique==true&&self中没有arg
*/
if ( !options.unique || !self.has( arg ) ) {
list.push( arg );
}
} else if ( arg && arg.length && jQuery.type( arg ) !== "string" ) {
// 传参方式为add([fn...]) 递归
// Inspect recursively
add( arg );
}
} );
} )( arguments ); //arguments为参数数组 所以add的第一步是each遍历
//添加到list后若memory真则fire,此时firingIndex为回调队列的最后一个函数
if ( memory && !firing ) {
fire();
}
}
return this;
}
fire
、fireWith
方法内部实际调用了局部函数fire
,其代码如下。触发时,需要更新fired
和firing
,表示已触发和正在触发。通过for循环执行队里中的函数。结束循环后,将firingIndex
更新为-1,表示下次触发从队列中的第一个函数开始。遍历在fireWith
中更新过的queue
,queue
是保存数组的数组,每个数组的第一个元素是context
,第二个元素是参数数组。执行函数时要是否返回false
且回调对象有stopOnFalse
标志,如果是则停止触发。
// Fire callbacks
fire = function() {
// Enforce single-firing
// 执行单次触发
locked = locked || options.once;
// Execute callbacks for all pending executions,
// respecting firingIndex overrides and runtime changes
// 标记已触发和正在触发
fired = firing = true;
// 循环调用list中的回调函数
// 循环结束之后 firingIndex赋-1 下一次fire从list的第一个开始 除非firingIndex被修改过
// 若设置了memory,add的时候会修改firingIndex并调用fire
// queue在fireWith函数内被更新,保存了触发函数的context和参数
for ( ; queue.length; firingIndex = -1 ) {
memory = queue.shift();
while ( ++firingIndex < list.length ) {
// Run callback and check for early termination
// memory[0]是content memory[1]是参数
if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false &&
options.stopOnFalse ) {
// Jump to end and forget the data so .add doesn't re-fire
// 当前执行函数范围false且options.stopOnFalse==true 直接跳至list尾 终止循环
firingIndex = list.length;
memory = false;
}
}
}
// 没设置memory时不保留参数
// 设置了memory时 参数仍保留在其中
// Forget the data if we're done with it
if ( !options.memory ) {
memory = false;
}
firing = false;
// Clean up if we're done firing for good
if ( locked ) {
// Keep an empty list if we have data for future add calls
if ( memory ) {
list = [];
// Otherwise, this object is spent
} else {
list = "";
}
}
},