jQuery回调、递延对象总结(一)jQuery.Callbacks详解
前言: 作为参数传递给另一个函数执行的函数我们称为回调函数,那么该回调又是否是异步的呢,何谓异步,如:作为事件处理器,或作为参数传递给 (setTimeout,setInterval)这样的异步函数,或作为ajax发送请求,应用于请求各种状态的处理,我们可以称为异步回调,jQuery.Callbacks 为我们封装了一个回调对象模块,我们先来看一个应用场景: // 为什么jQuery中的ready事件可以执行多个回调,这得益于我们的jQuery.Deferred递延对象(是基于jQuery.Callbacks回调模块) jQuery(function($) { console.log('document is ready!'); // do something }); jQuery(function($) { // do something }); // 实现原型 // jQuery.Deferred版代码 var df = jQuery.Deferred(); df.resolve(); // 在ready事件中调用 // 可以多次执行绑定的函数 df.done(fn1, fn2, fn3); df.done(fn4); // ... // jQuery.Callbacks版代码 var cb = jQuery.Callbacks('once memory'); cb.fire(); // 在ready事件中调用 // 可以多次执行绑定的函数 cb.add(fn1, fn2, fn3); cb.add(fn4); // ... 现在我们知道jQuery中的ready事件是可以这样执行多个回调的,要想深入理解其源码,让我们继续看下面吧 一、jQuery.Callbacks设计思路 使用一个私有变量list(数组)存储回调,执行jQuery.Callbacks函数将返回一个可以操作回调列表list的接口对象, 而传入jQuery.Callbacks函数的options参数则用来控制返回的回调对象操作回调列表的行为 回调对象中的方法 { add: 增加回调到list中 remove: 从list中移除回调 fire: 触发list中的回调 fired: 回调对象是否执行过fire方法 fireWith: 触发list中的回调,第一个参数为执行域 has: 判断函数是否在list中 empty: 将list致空,list = []; lock: 锁定list locked: 是否锁定 disable: 禁用回调对象 disabled: 是否禁用 } 参数标志: options = { once: 回调对象仅触发(fire)一次 memory: 跟踪记录每一次传递给fire函数的参数,在回调对象触发后(fired), 将最后一次触发(fire)时的参数(value)传递给在add操作后即将被调用的回调 unique: 在add操作中,相同的函数仅只一次被添加(push)到回调列表中 stopOnFalse:当回调函数返回false,中断列表中的回调循环调用,且memory === false,阻止在add操作中将要触发的回调 }
二、源码解析 var // Used for splitting on whitespace core_rnotwhite = /\S+/g; var optionsCache = {}; // Convert String-formatted options into Object-formatted ones and store in cache // 将字符串格式选项转化成对象格式形式,并存储在缓存对象optionsCache[options]中 // 该缓存起作用适用于执行多次jQuery.Callbacks函数,且传递options参数一致,我们在jQuery.Deferred // 源码就可以看到tuples二维数组中执行了两次jQuery.Callbacks('once memory') function createOptions( options ) { var object = optionsCache[ options ] = {}; jQuery.each( options.match( core_rnotwhite ) || [], function( _, flag ) { object[ flag ] = true; }); return object; } 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 // Flag to know if list is currently firing firing, // Last fire value (for non-forgettable lists) memory, // Flag to know if list was already fired fired, // End of the loop when firing firingLength, // Index of currently firing callback (modified by remove if needed) firingIndex, // First callback to fire (used internally by add and fireWith) firingStart, // 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; // 迭代list回调列表,列表中的回调被应用(或执行回调) for ( ; list && firingIndex < firingLength; firingIndex++ ) { // 如果回调返回false,且options.stopOnFlase === true,则中断循环 // 注:data[1]是一个伪数组(self.fire方法中的arguments(参数集合)) if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) { memory = false; // To prevent further calls using add // 阻止在add操作中可能执行的回调 break; } } firing = false; if ( list ) { // (options.once === undefined),回调对象可以触发多次 if ( stack ) { // 处理正在执行的回调中的fireWith操作 // 注:如果执行的回调中真的拥有fire或fireWith操作,那么列表中的回调将会无限循环的执行,请看实例1 if ( stack.length ) { fire( stack.shift() ); } } // (options.once === true && options.memory === true) // 回调列表致空,但允许add继续添加并执行回调 else if ( memory ) { list = []; } // (options.once === true && options.memory === undefined) // 禁用回调对象 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" ) { // 回调不唯一 或 唯一且不存在,则push 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? // 如果正在执行的回调执行了add操作,更新firingLength,将列表中新加进来的最后一个回调加入到回调执行的队列中 if ( firing ) { firingLength = list.length; // With memory, if we're not firing then // we should call right away // 如果可能(options.memory === true),在回调对象不能再次fire(options.once === true)时, // 我们应该使用memory(记录的最后一次fire时的参数)立即调用回调 } 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 // 在回调对象触发(fire)时,如果firingLength、firingIndex(正在执行的回调在列表list中的索引index) // 大于等于 移除的回调的索引(index),分别减一,确保回调执行队列中未执行的回调依次执行 if ( firing ) { if ( index <= firingLength ) { firingLength--; } if ( index <= firingIndex ) { firingIndex--; } } } }); } return this; }, // Check if a given callback is in the list. // If no argument is given, return whether or not list has callbacks attached. // 检查给定的回调是否在列表中 // 如果未给定回调参数,返回列表是否有回调 has: function( fn ) { return fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length ); }, // Remove all callbacks from the list // 将列表致空,list = []; firingLenght = 0; empty: function() { list = []; firingLength = 0; return this; }, // Have the list do nothing anymore // 禁用回调对象 // 将list赋值为undefined就可以使self中的add,remove,fire,fireWith方法停止工作 // 我认为这里把stack、memory赋值为undefined与否是没有任何关系的 disable: function() { list = stack = memory = undefined; return this; }, // Is it disabled? disabled: function() { return !list; }, // Lock the list in its current state // 锁定回调列表 // 如果(fired !== true || options.memory === false),则视为禁用(disable) // 如果(fired === true && options.memory === true),则视为options.once === true // 请看实例2 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 ) { // 回调对象未执行过fire 或且 可以执行多次(options.once === false) // 如果(fired === true && options.once === true),则不会执行fire操作 if ( list && ( !fired || stack ) ) { args = args || []; args = [ context, args.slice ? args.slice() : args ]; 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; };
三、实例 实例1、 处理回调函数中的fire,或fireWidth操作 var cb = jQuery.Callbacks(); var fn1 = function(arg){ console.log( arg + '1' ); }; var fn2 = function(arg){ console.log( arg + '2' ); cb.fire(); }; var fn3 = function(arg){ console.log( arg + '3' ); }; cb.add(fn1, fn2, fn3); cb.fire('fn'); // 其中回调fn1,fn2,fn3无限制的循环调用 /* 控制台将无限制输出如下: fn1 fn2 fn3 fn1 fn2 fn3 fn1 fn2 fn3 . . . */ 实例2、 锁定(lock)操作各种场景中的用法 var cb1 = jQuery.Callbacks(); var cb2 = jQuery.Callbacks('memory'); var cb3 = jQuery.Callbacks('memory'); var fn1 = function(arg){ console.log( arg + '1' ); }; var fn2 = function(arg){ console.log( arg + '2' ); }; var fn3 = function(arg){ console.log( arg + '3' ); }; // 如果options.memory !== true,锁定操作视为禁用回调对象 cb1.add(fn1); cb1.lock(); // 以下操作无任何反应 cb1.add(fn2); cb1.fire('fn'); // 如果fried !== true,锁定操作也视为禁用回调对象 cb2.add(fn1); cb2.lock(); // 以下操作无任何反应 cb2.add(fn2); cb2.fire('fn'); // 如果(fired === true && options.memory === true),锁定操作类似控制标志once(options.once === true); cb3.add(fn1); cb3.fire('fn'); // fn1,此时fired === true cb3.lock(); // 像是传入了'once'标志,jQuery.Callbacks('once memory'); cb3.add(fn2); // fn2 cb3.fire('fn'); // 再次触发,无任何反应 cb3.add(fn3); // fn3 // 再来看看jQuery.Deferred中的一段源码 var tuples = [ // action, add listener, listener list, final state [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ], [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ], [ "notify", "progress", jQuery.Callbacks("memory") ] ]; // Handle state if ( stateString ) { list.add(function() { // state = [ resolved | rejected ] state = stateString; // [ reject_list | resolve_list ].disable; progress_list.lock }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); } /* 当执行了tuples中前面两组中任意一个回调对象的fire方法时,后一组回调对象被锁定, 相当于(fired === true && options.memory === true),后一组回调对象实际为执行 jQuery.Callbacks('once memory')生成的回调对象。 */