jQuery 之 Callback 實現


在 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;
};

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM