前言

上一节已经说过了,接下来的两节会介绍 jQuery 中是如何实现 promise 的,在真正步入 promise 实现之前,这一节我们需要为下一节做铺垫,先掌握 jQuery.Callbacks() 这一方法的作用、实现。

我们首先来看一下这一方法的定义以及使用方法。

定义

定义:jQuery.Callbacks() 指一个多用途的回调函数列表对象,提供了一种强大的方法来管理回调函数列对。

这一方法为 jQuery 内部许多功能的实现建立了基础功能,比如 .ajax还有我们将要介绍的 $.Deferred函数。

用法

对于这一方法的使用其实还是有很多细节的。

对于方法本身,由于它管理着一个函数列对,你现在可以想象它就是这个样子:



[function() {}, function() {}, function() {}, ...]



它管理着这些函数,所以我们可以有 addremovefire等操作,这里将来演示:



function eat() {
	console.log('eat');
}
function drink() {
	console.log('drink');
}
function sleep() {
	console.log('sleep');
}

var cb = $.Callbacks();
cb.add(eat);
cb.add(drink);
cb.add(sleep);
cb.remove(sleep);
cb.fire();



执行结果为:




jquery toggleClass完毕回调_回调函数


可以看到我们可以使用add方法为容器中添加函数,使用remove删除容器中的指定函数,使用fire方法时会自动调用这个容器中的所有函数。

使用时添加特性

我们除了上述的基本使用之外,还可以再创建容器的时候添加一些特性让其使用时具备某种性质。

支持的特性有:

  • once:容器中的所有回调函数都只执行一次。
  • memory:缓存上一次fire时的参数值,当add()添加回调函数时,直接用上一次的参数值立刻调用新加入的回调函数。
  • unique: 一个回调只会被添加一次,不会重复添加。
  • stopOnFalse: 某个回调函数返回false之后中断后面的回调函数。

具体演示如下,首先对once进行演示:


function eat() {
	console.log('eat');
}
function drink() {
	console.log('drink');
}
function sleep() {
	console.log('sleep');
}

var cb = $.Callbacks('once');
cb.add(eat);
cb.add(drink);
cb.add(sleep);
cb.fire();
cb.fire();


这里调用了两次fire方法,默认容器中的回调函数都会被执行两次,但是由于添加了特性once,所以最终只执行了一次:


jquery toggleClass完毕回调_jQuery_02


下面演示memory特性:


function eat() {
	console.log('eat');
}
function drink() {
	console.log('drink');
}
function sleep() {
	console.log('sleep');
}

var cb = $.Callbacks('memory');
cb.add(eat);
cb.add(drink);
cb.fire();
cb.add(sleep);


可以看到添加sleep函数后并没有再次执行fire但是sleep就已经执行了:


jquery toggleClass完毕回调_权限管理_03


接下来是unique特性:


function eat() {
	console.log('eat');
}
function drink() {
	console.log('drink');
}

var cb = $.Callbacks('unique');
cb.add(eat);
cb.add(eat);
cb.add(drink);
cb.fire();


这里添加了两次 eat函数,但是由于使用了特性unique所以实际上容器中只有一个 eat函数,所以执行结果为:


jquery toggleClass完毕回调_回调函数_04


最后一个特性是stopOnFalse


function eat() {
	console.log('eat');
}
function drink() {
	console.log('drink');
	return false;
}
function sleep() {
	console.log('sleep');
}

var cb = $.Callbacks('stopOnFalse');
cb.add(eat);
cb.add(drink);
cb.add(sleep);
cb.fire();


这里添加了三个函数,但是由于使用了stopOnFalse特性并且在第二个函数的内部返回结果为false,所以就会停止对后面的回调函数的执行,所以执行结果为:


jquery toggleClass完毕回调_回调函数_05


组合使用

有些时候,我们甚至可以将这些特性组合使用,可以达到一些特殊的效果,这里不再演示。

进入源码

了解完该方法的使用方法及其特性后,让我们一起走进源码一探究竟:


jQuery.Callbacks = function( options ) {

	// 处理特性参数
	options = typeof options === "string" ?
		createOptions( options ) :
		jQuery.extend( {}, options );

	var // Flag to know if list is currently firing
		firing,

		// Last fire value for non-forgettable lists
		memory,

		// 标记容器中的函数队列是否已经被 fire 过了
		fired,

		// Flag to prevent firing
		locked,

		// 存放函数的容器
		list = [],

		// 函数队列,先进先出,先进来的先执行
		queue = [],

		// Index of currently firing callback (modified by add/remove as needed)
		firingIndex = -1,

		// 该方法用于真实的执行容器中的函数
		fire = function() {

			// Enforce single-firing
			locked = locked || options.once;

			// Execute callbacks for all pending executions,
			// respecting firingIndex overrides and runtime changes
			fired = firing = true;
			for ( ; queue.length; firingIndex = -1 ) {
				// queue 的长度每次循环都减一,用于获取当前需要执行的函数并且控制循环次数
				memory = queue.shift();
				while ( ++firingIndex < list.length ) {

					// 执行函数 注意使用 apply 可以正确传参
					if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false &&
						options.stopOnFalse ) {

						// Jump to end and forget the data so .add doesn't re-fire
						firingIndex = list.length;
						memory = false;
					}
				}
			}

			// 判断用户是否添加了特性 memory
			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 = "";
				}
			}
		},

		// self 就是调用 $.Callbacks() 的结果,可以为其添加供外部使用的方法
		self = {

			// add 方法用于添加回调函数到队列中
			add: function() {
				if ( list ) {

					// 如果使用了 memory 特性,就要在 add 后立刻调用该函数
					if ( memory && !firing ) {
						firingIndex = list.length - 1;
						queue.push( memory );
					}

					// 用于判断是否使用了 unique 特性,如果使用了则实现唯一
					( function add( args ) {
						jQuery.each( args, function( _, arg ) {
							if ( isFunction( arg ) ) {
								if ( !options.unique || !self.has( arg ) ) {
									list.push( arg );
								}
							} else if ( arg && arg.length && toType( arg ) !== "string" ) {

								// 递归检查
								add( arg );
							}
						} );
					} )( arguments );

					if ( memory && !firing ) {
						fire();
					}
				}
				// return this 很关键,用于后续的链式调用
				return this;
			},

			// remove 函数用于删除该容器中的函数
			remove: function() {
				jQuery.each( arguments, function( _, arg ) {
					var index;

					// 如果容器中存在该函数,就删除
					while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {
						list.splice( index, 1 );

						// 设置 firing 索引
						if ( index <= firingIndex ) {
							firingIndex--;
						}
					}
				} );
				// 用于链式操作
				return this;
			},

			// 判断所给函数是否在容器中,如果不传参,返回容器中是否具有函数
			has: function( fn ) {
				return fn ?
					jQuery.inArray( fn, list ) > -1 :
					list.length > 0;
			},

			// empty 方法用于清空容器中的所有函数
			// 这里直接让 list 获得一个空数组的引用,之前的数组则被垃圾回收机制回收
			empty: function() {
				if ( list ) {
					list = [];
				}
				return this;
			},

			// Disable .fire and .add
			// Abort any current/pending executions
			// Clear all callbacks and values
			disable: function() {
				locked = queue = [];
				list = memory = "";
				return this;
			},
			disabled: function() {
				return !list;
			},

			// Disable .fire
			// Also disable .add unless we have memory (since it would have no effect)
			// Abort any pending executions
			lock: function() {
				locked = queue = [];
				if ( !memory && !firing ) {
					list = memory = "";
				}
				return this;
			},
			locked: function() {
				return !!locked;
			},

			// 依次调用容器中的所有回调函数并且绑定上下文 ,可以传参
			fireWith: function( context, args ) {
				if ( !locked ) {
					args = args || []; // 如果没有传参,args 为空 []
					args = [ context, args.slice ? args.slice() : args ];
					queue.push( args );
					if ( !firing ) {
						// 执行内部的 fire 方法
						fire();
					}
				}
				return this;
			},

			// 依次调用容器中的所有回调函数,可以传参
			fire: function() {
				// 调用内部 fireWith 方法,并且传递上下文 this 和调用时传递的参数
				self.fireWith( this, arguments );
				return this;
			},

			// 返回队列中的函数是否已经被 fire 过一次
			fired: function() {
				return !!fired;
			}
		};

	// 返回操作容器的所有方法
	return self;
};


其中调用了一个 createOptions函数,该函数用于格式化用户传进来的特性参数为对象形式。其实现如下:


function createOptions( options ) {
	var object = {};
	jQuery.each( options.match( rnothtmlwhite ) || [], function( _, flag ) {
		object[ flag ] = true;
	} );
	return object;
}


解读完源码后我们要对 jQuery.Callbacks() 有更加明确的认识,他就是可以创建一个函数队列,我们可以为其中添加回调函数,删除回调函数等,最后可以调用fire方法去依次执行队列中的所有函数。另外我们可以为容器添加一些特性,来控制容器的特性。

感悟

通过对 jQuery.Callbacks()

  • 对参数的处理我们可以选择合适的数据结构对其进行格式化方便后续的访问
  • 对于每一项权限管理,我们都可以创建一个新的变量来记录状态来实现权限管理
  • return this是实现链式调用的核心代码,原理即是每次调用函数后都返回调用者
  • 熟练使用 callapply来绑定上下文 this 的同时进行正确的传参