在 js 開發中,由於沒有多線程,經常會遇到回調這個概念,比如說,在 ready 函數中注冊回調函數,注冊元素的事件處理等等。在比較復雜的場景下,當一個事件發生的時候,可能需要同時執行多個回調方法,可以直接考慮到的實現就是實現一個隊列,將所有事件觸發時需要回調的函數都加入到這個隊列中保存起來,當事件觸發的時候,從這個隊列重依次取出保存的函數並執行。
可以如下簡單的實現。
首先,實現一個類函數來表示這個回調類。在 javascript 中,使用數組來表示這個隊列。
function Callbacks() { this.list = []; }
然后,通過原型實現類中的方法。增加和刪除的函數都保存在數組中,fire 的時候,可以提供參數,這個參數將會被傳遞給每個回調函數。
Callbacks.prototype = { add: function(fn) { this.list.push(fn); }, remove: function(fn){ var position = this.list.indexOf(fn); if( position >=0){ this.list.splice(position, 1); } }, fire: function(args){ for(var i=0; i<this.list.length; i++){ var fn = this.list[i]; fn(args); } } };
測試代碼如下:
function fn1(args){ console.log("fn1: " + args); } function fn2(args){ console.log("fn2: " + args); } var callbacks = new Callbacks(); callbacks.add(fn1); callbacks.fire("Alice"); callbacks.add(fn2); callbacks.fire("Tom"); callbacks.remove(fn1); callbacks.fire("Grace");
或者,不使用原型,直接通過閉包來實現。
function Callbacks() { var list = []; return { add: function(fn) { list.push(fn); }, remove: function(fn){ var position = list.indexOf(fn); if( position >=0){ list.splice(position, 1); } }, fire: function(args) { for(var i=0; i<list.length; i++){ var fn = list[i]; fn(args); } } }; }
這樣的話,示例代碼也需要調整一下。我們直接對用 Callbacks 函數就可以了。
function fn1(args){ console.log("fn1: " + args); } function fn2(args){ console.log("fn2: " + args); } var callbacks = Callbacks(); callbacks.add(fn1); callbacks.fire("Alice"); callbacks.add(fn2); callbacks.fire("Tom"); callbacks.remove(fn1); callbacks.fire("Grace");
下面我們使用第二種方式繼續進行。
對於更加復雜的場景來說,我們需要只能 fire 一次,以后即使調用了 fire ,也不再生效了。
比如說,可能在創建對象的時候,成為這樣的形式。這里使用 once 表示僅僅能夠 fire 一次。
var callbacks = Callbacks("once");
那么,我們的代碼也需要進行一下調整。其實很簡單,如果設置了 once,那么,在 fire 之后,將原來的隊列中直接干掉就可以了。
function Callbacks(options) { var once = options === "once"; var list = []; return { add: function(fn) { if(list){ list.push(fn); } }, remove: function(fn){ if(list){ var position = list.indexOf(fn); if( position >=0){ list.splice(position, 1); } } }, fire: function(args) { if(list) { for(var i=0; i<list.length; i++){ var fn = list[i]; fn(args); } } if( once ){ list = undefined; } } }; }
jQuery 中,不只提供了 once 一種方式,而是提供了四種類型的不同方式:
- once: 只能夠觸發一次。
- memory: 當隊列已經觸發之后,再添加進來的函數就會直接被調用,不需要再觸發一次。
- unique: 保證函數的唯一
- stopOnFalse: 只要有一個回調返回 false,就中斷后繼的調用。
這四種方式可以組合,使用空格分隔傳入構造函數即可,比如 $.Callbacks("once memory unique");
官方文檔中,提供了一些使用的示例。
callbacks.add(fn1, [fn2, fn3,...])//添加一個/多個回調
callbacks.remove(fn1, [fn2, fn3,...])//移除一個/多個回調
callbacks.fire(args)//觸發回調,將args傳遞給fn1/fn2/fn3……
callbacks.fireWith(context, args)//指定上下文context然后觸發回調
callbacks.lock()//鎖住隊列當前的觸發狀態
callbacks.disable()//禁掉管理器,也就是所有的fire都不生效
由於構造函數串實際上是一個字符串,所以,我們需要先分析這個串,構建為一個方便使用的對象。
// 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.match( rnotwhite ) || [], function( _, flag ) { object[ flag ] = true; }); return object; }
這個函數將如下的參數 "once memory unique" 轉換為一個對象。
{ once: true, memory: true, unique: true }
再考慮一些特殊的情況,比如在 fire 處理隊列中,某個函數又在隊列中添加了一個回調函數,或者,在隊列中又刪除了某個回調函數。為了處理這種情況,我們可以在遍歷整個隊列的過程中,記錄下當前處理的起始下標、當前處理的位置等信息,這樣,我們就可以處理類似並發的這種狀況了。
// 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 = [],
如果在 fire 處理過程中,某個函數又調用了 fire 來觸發事件呢?
我們可以將這個嵌套的事件先保存起來,等到當前的回調序列處理完成之后,再檢查被保存的事件,繼續完成處理。顯然,使用隊列是處理這種狀況的理想數據結構,如果遇到這種狀況,我們就將事件數據入隊,待處理的時候,依次出隊數據進行處理。什么時候需要這種處理呢?顯然不是在 once 的狀況下。在 JavaScript 中,堆隊列也是通過數組來實現的,push 用來將數據追加到數組的最后,而 shift 用來出隊,從數組的最前面獲取數據。
不過,jQuery 沒有稱為隊列,而是稱為了 stack.
// Stack of fire calls for repeatable lists stack = !options.once && [],
入隊代碼。
if ( firing ) { stack.push( args ); }
出隊代碼。
if ( list ) { if ( stack ) { if ( stack.length ) { fire( stack.shift() ); } } else if ( memory ) { list = []; } else { self.disable(); } }
先把基本結構定義出來,函數的開始定義我們使用的變量。
jQuery.Callbacks = function( options ) { var options = createOptions(options); var memory, // Flag to know if list was already fired // 是否已經 fire 過 fired, // Flag to know if list is currently firing // 當前是否還處於 firing 過程中 firing, // First callback to fire (used internally by add and fireWith) // fire 調用的起始下標 firingStart, // End of the loop when firing // 需要 fire 調用的隊列長度 firingLength, // Index of currently firing callback (modified by remove if needed) // 當前正在 firing 的回調在隊列的索引 firingIndex, // Actual callback list // 回調隊列 list = [], // Stack of fire calls for repeatable lists // 如果不是 once 的,stack 實際上是一個隊列,用來保存嵌套事件 fire 所需的上下文跟參數 stack = !options.once && [], _fire = function( data ) { }; var self = { add : function(){}, remove : function(){}, has : function(){}, empty : function(){}, fireWith : function(context, args){ _fire([context, args]); }; fire : function(args){ this.fireWith(this, args); } /* other function */ } return self; };
其中的 stack 用來保存在 fire 之后添加進來的函數。
而 firingIndex, firingLength 則用來保證在調用函數的過程中,我們還可以對這個隊列進行操作。實現並發的處理。
我們從 add 函數開始。
add: function() { if ( list ) { // 如果使用了 once,在觸發完成之后,就不能再添加回調了。 // 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? 正在 firing 中,隊列長度發生了變化 if ( firing ) { firingLength = list.length; // With memory, if we're not firing then // we should call right away 如果是 memory 狀態,而且已經觸發過了,直接觸發, memory 是保存了上次觸發的狀態 } else if ( memory ) { firingStart = start; fire( memory ); } } return this; },
刪除就簡單一些了。檢查准備刪除的函數是否在隊列中,while 的作用是一個回調可能被多次添加到隊列中。
// 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; },
has, empty, disable, disabled 比較簡單。
// 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 empty: function() { list = []; firingLength = 0; return this; }, // Have the list do nothing anymore disable: function() { list = stack = memory = undefined; return this; }, // Is it disabled? disabled: function() { return !list; },
鎖住的意思其實是不允許再觸發事件,stack 本身也用來表示是否禁止再觸發事件。所以,通過直接將 stack 設置為 undefined,就關閉了再次觸發事件的可能。
// Lock the list in its current state lock: function() { stack = undefined; if ( !memory ) { self.disable(); } return this; }, // Is it locked? locked: function() { return !stack; },
fire 是暴露的觸發方法。fireWith 則可以指定當前的上下文,也就是回調函數中使用的 this 。第一行的 if 判斷中表示了觸發事件的條件,必須存在 list,必須有 stack 或者還沒有觸發過。
// Call all callbacks with the given context and arguments fireWith: function( context, args ) { 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; } };
真正的 fire 函數。
// 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(); } } },
jQuery-2.1.3.js 中的 Callback 實現。
/* * 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; }, // 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 empty: function() { list = []; firingLength = 0; 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 ) { 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; };
