jQuery 之 Callback 實現

在 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 一種方式,而是提供了四種類型的不一樣方式:

  • 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;
};
相關文章
相關標籤/搜索