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,添加后,直接触发