jquery源码之低调的回调函数队列--Callbacks

jQuery中有一个很实用的函数队列,可能我们很少用到,但他在jQuery内部却有着举足轻重的地位。

他就是Callbacks. jQuery作者用它构建了很多非常重要的模块。比如说$.Deferred。

Callbacks 说白了就是个数组,里面存了很多函数对象。然而他真的 just so so么?

好吧,爱因斯坦也只是个人,但他真的仅仅是个普普通通的人吗?Callbacks也不是。

不说废话了,先看源码。

// String to Object options format cache
var optionsCache = {};

// Convert String-formatted options into Object-formatted ones and store in cache
function createOptions( options ) {
	var object = optionsCache[ options ] = {};
	jQuery.each( options.split( core_rspace ), function( _, flag ) {
		object[ flag ] = true;
	});
	return object;
}

/*
 * Create a callback list using the following parameters:
 *
 *	options: an optional list of space-separated options that will change how
 *			the callback list behaves or a more traditional option object
 *
 * By default a callback list will act like an event callback list and can be
 * "fired" multiple times.
 *
 * Possible options:
 *
 *	once:			will ensure the callback list can only be fired once (like a Deferred)
 *
 *	memory:			will keep track of previous values and will call any callback added
 *					after the list has been fired right away with the latest "memorized"
 *					values (like a Deferred)
 *
 *	unique:			will ensure a callback can only be added once (no duplicate in the list)
 *
 *	stopOnFalse:	interrupt callings when a callback returns false
 *
 */
jQuery.Callbacks = function( options ) {

	// Convert options from String-formatted to Object-formatted if needed
	// (we check in cache first)
	options = typeof options === "string" ?
		( optionsCache[ options ] || createOptions( options ) ) :
		jQuery.extend( {}, options );

	var // Last fire value (for non-forgettable lists)
		memory,
		// Flag to know if list was already fired
		fired,
		// Flag to know if list is currently firing
		firing,
		// First callback to fire (used internally by add and fireWith)
		firingStart,
		// End of the loop when firing
		firingLength,
		// Index of currently firing callback (modified by remove if needed)
		firingIndex,
		// Actual callback list
		list = [],
		// Stack of fire calls for repeatable lists
		stack = !options.once && [],
		// Fire callbacks
		fire = function( data ) {
			memory = options.memory && data;
			fired = true;
			firingIndex = firingStart || 0;
			firingStart = 0;
			firingLength = list.length;
			firing = true;
			for ( ; list && firingIndex < firingLength; firingIndex++ ) {
				if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) {
					memory = false; // To prevent further calls using add
					break;
				}
			}
			firing = false;
			if ( list ) {
				if ( stack ) {
					if ( stack.length ) {
						fire( stack.shift() );
					}
				} else if ( memory ) {
					list = [];
				} else {
					self.disable();
				}
			}
		},
		// Actual Callbacks object
		self = {
			// Add a callback or a collection of callbacks to the list
			add: function() {
				if ( list ) {
					// First, we save the current length
					var start = list.length;
					(function add( args ) {
						jQuery.each( args, function( _, arg ) {
							var type = jQuery.type( arg );
							if ( type === "function" ) {
								if ( !options.unique || !self.has( arg ) ) {
									list.push( arg );
								}
							} else if ( arg && arg.length && type !== "string" ) {
								// Inspect recursively
								add( arg );
							}
						});
					})( arguments );
					// Do we need to add the callbacks to the
					// current firing batch?
					if ( firing ) {
						firingLength = list.length;
					// With memory, if we're not firing then
					// we should call right away
					} else if ( memory ) {
						firingStart = start;
						fire( memory );
					}
				}
				return this;
			},
			// Remove a callback from the list
			remove: function() {
				if ( list ) {
					jQuery.each( arguments, function( _, arg ) {
						var index;
						while( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {
							list.splice( index, 1 );
							// Handle firing indexes
							if ( firing ) {
								if ( index <= firingLength ) {
									firingLength--;
								}
								if ( index <= firingIndex ) {
									firingIndex--;
								}
							}
						}
					});
				}
				return this;
			},
			// Control if a given callback is in the list
			has: function( fn ) {
				return jQuery.inArray( fn, list ) > -1;
			},
			// Remove all callbacks from the list
			empty: function() {
				list = [];
				return this;
			},
			// Have the list do nothing anymore
			disable: function() {
				list = stack = memory = undefined;
				return this;
			},
			// Is it disabled?
			disabled: function() {
				return !list;
			},
			// Lock the list in its current state
			lock: function() {
				stack = undefined;
				if ( !memory ) {
					self.disable();
				}
				return this;
			},
			// Is it locked?
			locked: function() {
				return !stack;
			},
			// Call all callbacks with the given context and arguments
			fireWith: function( context, args ) {
				args = args || [];
				args = [ context, args.slice ? args.slice() : args ];
				if ( list && ( !fired || stack ) ) {
					if ( firing ) {
						stack.push( args );
					} else {
						fire( args );
					}
				}
				return this;
			},
			// Call all the callbacks with the given arguments
			fire: function() {
				self.fireWith( this, arguments );
				return this;
			},
			// To know if the callbacks have already been called at least once
			fired: function() {
				return !!fired;
			}
		};

	return self;
};

 代码只有仅仅200行不到,但真正看起来却又点绕,

《think in java》中有这么一句,理解一个程序最好的方法,就是把它看做一个服务的提供者。

那他提供了那些服务:

首先我们看看返回的self对象

{
	// 添加方法
	add: function() {},
	// 删除
	remove: function() {},
	// 是否包含
	has: function() {},
	// 清空
	empty: function() {},
	// 禁用
	disable: function() {},
	// 加锁
	lock: function() {},
	// 是否加锁
	locked: function() {},
	// 触发
	fireWith: function(){},
	fire: function() {},
	// 是否触发
	fired: function() {}
}

  用途都十分清晰,那我们再看看参数,程序是服务的提供者,那么参数作为程序的入口的携带者,一般会用来装配一些属性。

显然这里就是这样。

 先看Callbacks内部关于参数部分的代码。

        // 官方注释,将配置的options由string格式转换为object格式如果需要的话
        // Convert options from String-formatted to Object-formatted if needed
	// (we check in cache first)
	options = typeof options === "string" ?
		// 注意这里, 这里去取optionsCache的值,或者调用
		( optionsCache[ options ] || createOptions( options ) ) :
		jQuery.extend( {}, options );

  在看看createOptions方法吧,其实就是个转换方法,还带有缓存功能。

// String to Object options format cache
// 建立一个缓存对象
var optionsCache = {};

// Convert String-formatted options into Object-formatted ones and store in cache
function createOptions( options ) {
	// 创建optionsCache中的options属性
	var object = optionsCache[ options ] = {};
	// 这里用到 each方法遍历
	// options.split( core_rspace )  根据空格划分为数组
	// _在jquery中通常用来作为占位符,即忽略的参数
	jQuery.each( options.split( core_rspace ), function( _, flag ) {
		// 遍历以后将切割后的每个属性设置为true
		object[ flag ] = true;
	});
	return object;
}
// 可能例子会更清晰,
var obj = createOptions( "once memory");
/*
obj;
{
	once: true,
	memory: true
}
*/    

  接下来就是具体的实现了,jQuery的实现一直是十分巧妙的,当然这可能仅仅是小菜我看来。

/*
 * Create a callback list using the following parameters:
 *
 *	options: an optional list of space-separated options that will change how
 *			the callback list behaves or a more traditional option object
 *
 * By default a callback list will act like an event callback list and can be
 * "fired" multiple times.
 *
 * Possible options:
 *
 *	once:			will ensure the callback list can only be fired once (like a Deferred)
 *
 *	memory:			will keep track of previous values and will call any callback added
 *					after the list has been fired right away with the latest "memorized"
 *					values (like a Deferred)
 *
 *	unique:			will ensure a callback can only be added once (no duplicate in the list)
 *
 *	stopOnFalse:	interrupt callings when a callback returns false
 *
 */
 //
jQuery.Callbacks = function( options ) {

	// Convert options from String-formatted to Object-formatted if needed
	// (we check in cache first)
	options = typeof options === "string" ?
		// 注意这里, 这里去取optionsCache的值,或者调用createOptions
		// 我们看看createOptions函数
		( optionsCache[ options ] || createOptions( options ) ) :
		jQuery.extend( {}, options );

	var // Last fire value (for non-forgettable lists)
		// 以前触发的值(为了记忆的list,记忆了上次调用时所传递的基本信息(即记忆了参数))
		memory,
		// 是否触发
		// Flag to know if list was already fired
		fired,
		// 是否正在触发
		// Flag to know if list is currently firing
		firing,
		// 第一个被触发的function
		// First callback to fire (used internally by add and fireWith)
		firingStart,
		// 触发列表的长度
		// End of the loop when firing
		firingLength,
		// 当前触发的索引
		// Index of currently firing callback (modified by remove if needed)
		firingIndex,
		// 内部存放function的数组
		// Actual callback list
		list = [],
		// 用来存放重复调用的数组,(当Callbacks被配置了 once属性,则为false)
		// Stack of fire calls for repeatable lists
		stack = !options.once && [],
		// 内部触发函数,这里看到jquery隐藏信息的习惯了
		// 作为该模块的核心方法
		// 它没有暴露给外部,
		// 《代码大全》 有提到信息隐藏的好处。
		// Fire callbacks
		fire = function( data ) {
			// 在设置memory的情况下为 传递过来的参数data, 否则为undefined
			memory = options.memory && data;
			// 进入到这时标记已触发
			fired = true;
			// 当前触发索引设置为开始,或者0
			firingIndex = firingStart || 0;
			firingStart = 0;
			firingLength = list.length;
			firing = true;
			// for循环触发list中的函数
			for ( ; list && firingIndex < firingLength; firingIndex++ ) {
				// 如果stopOnFalse被设置,则检查调用函数后是否返回false
				// 如果返回则终止触发,
				// 注意触发参数 为一个多维数组
				// data = [
				//	context,
				//	[args]
				//]  这应该是由外部封装成固定格式,再传递过来的参数
				if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) {
					memory = false; // To prevent further calls using add
					break;
				}
			}
			// 设置正在触发为false
			firing = false;
			// 如果list是存在的,即改callbacks还没有被禁用
			if ( list ) {
				// 如果 stack中有值,则递归调用
				// 其实这里是判断是否设置了once属性
				if ( stack ) {
					if ( stack.length ) {
						fire( stack.shift() );
					}
				} else if ( memory ) { // 如果设置记忆功能,则清空list(注意,是记忆需要调用的基本信息,即相关参数)
					list = [];
				} else {
					// 只能调用一次,且不能使用memory,
					// 则禁用
					self.disable();
				}
			}
		},
		// 再来看看需要暴露的对象
		// Actual Callbacks object
		self = {
			// 添加方法
			// Add a callback or a collection of callbacks to the list
			add: function() {
				// list其实是可以作为是否禁用的标志的,
				// 如果list存在
				if ( list ) {
					// First, we save the current length
					var start = list.length;
					// 真正的添加行为
					// 用到了自执行
					// 但又不是匿名函数,因为它可能需要递归
					(function add( args ) {
						jQuery.each( args, function( _, arg ) {
							var type = jQuery.type( arg );
							if ( type === "function" ) {
								// 如果设置了唯一,且当前已包含该函数,
								// 则不添加,反之则添加函数
								if ( !options.unique || !self.has( arg ) ) {
									list.push( arg );
								}
							} else if ( arg && arg.length && type !== "string" ) { // 递归调用
								// Inspect recursively
								add( arg );
							}
						});
					})( arguments );
					// Do we need to add the callbacks to the
					// current firing batch?
					// 如果正在触发,则只需要更新firingLength
					if ( firing ) {
						firingLength = list.length;
					// With memory, if we're not firing then
					// we should call right away
					// 如果memory,则在添加的时候直接触发
					} else if ( memory ) {
						firingStart = start;
						fire( memory );
					}
				}
				return this;
			},
			// Remove a callback from the list
			// 删除方法,遍历删除指定的方法,并维护好firingLength以及firingIndex
			remove: function() {
				if ( list ) {
					jQuery.each( arguments, function( _, arg ) {
						var index;
						while( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {
							list.splice( index, 1 );
							// Handle firing indexes
							if ( firing ) {
								if ( index <= firingLength ) {
									firingLength--;
								}
								if ( index <= firingIndex ) {
									firingIndex--;
								}
							}
						}
					});
				}
				return this;
			},
			// Control if a given callback is in the list
			// 是否包含
			has: function( fn ) {
				return jQuery.inArray( fn, list ) > -1;
			},
			// Remove all callbacks from the list
			empty: function() {
				list = [];
				return this;
			},
			// Have the list do nothing anymore
			disable: function() {
				list = stack = memory = undefined;
				return this;
			},
			// Is it disabled?
			disabled: function() {
				// 看,这里有用到list是否存在来判断 是否被禁用
				return !list;
			},
			// Lock the list in its current state
			// 锁住即不能再被触发
			// 如果没有设置memory则直接禁用
			lock: function() {
				stack = undefined;
				if ( !memory ) {
					self.disable();
				}
				return this;
			},
			// 是否加锁
			// Is it locked?
			locked: function() {
				// 居然是判断stack是否存在
				// 由此推断 加锁应该是设置智能触发一次
				return !stack;
			},
			// Call all callbacks with the given context and arguments
			fireWith: function( context, args ) {
				args = args || [];
				// 看这封装了arguments,用来内部fire函数的调用
				args = [ context, args.slice ? args.slice() : args ];
				// 如果还没被触发,或者允许触发多次
				if ( list && ( !fired || stack ) ) {
					// 正在触发,则添加到stack
					// 在当次触发后,直接触发
					if ( firing ) {
						stack.push( args );
					} else {
						// 直接触发
						fire( args );
					}
				}
				return this;
			},
			// Call all the callbacks with the given arguments
			// 设置context为this
			fire: function() {
				self.fireWith( this, arguments );
				return this;
			},
			// To know if the callbacks have already been called at least once
			fired: function() {
				return !!fired;
			}
		};
	// 注意有一个细节,self的所有方法都是返回的this
	// 这表明,它是支持链式操作的
	// jquery 很多地方用了这种优雅的技术
	return self;
};

  好吧,Callbacks就讲到这里了,神奇而低调的函数队列,在以后的源码中你也会经常看到他的身影,所以他能做什么并不用着急。

但还是举些小例子用用看:

var c = $.Callbacks("once memory");
c.add(function(i) {
	alert(123 + '-' + i);
});
c.add(function(i) {
	alert(234 + '-' + i);
});
c.add(function(i) {
	alert(456 + '-' + i);
});
c.fire('tianxia');
// alert('123-tianxi'); alert('234-tianxi'); alert('456-tianxi'); 
c.fire();
// 再次调用,啥都没发生,因为设置了once
// 什么都没发生
c.add(function(i) {
	alert(i);
});
// alert('tianxia')
// 在设置memory,添加后,直接触发