在 js 開發中,因爲沒有多線程,常常會遇到回調這個概念,好比說,在 ready 函數中註冊回調函數,註冊元素的事件處理等等。在比較複雜的場景下,當一個事件發生的時候,可能須要同時執行多個回調方法,能夠直接考慮到的實現就是實現一個隊列,將全部事件觸發時須要回調的函數都加入到這個隊列中保存起來,當事件觸發的時候,從這個隊列重依次取出保存的函數並執行。javascript
能夠以下簡單的實現。java
首先,實現一個類函數來表示這個回調類。在 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");
下面咱們使用第二種方式繼續進行。app
對於更加複雜的場景來講,咱們須要只能 fire 一次,之後即便調用了 fire ,也再也不生效了。函數
好比說,可能在建立對象的時候,成爲這樣的形式。這裏使用 once 表示僅僅可以 fire 一次。oop
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 一種方式,而是提供了四種類型的不一樣方式:
這四種方式能夠組合,使用空格分隔傳入構造函數便可,好比 $.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; };