• 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;
    };
  • 相关阅读:
    LeetCode153 Find Minimum in Rotated Sorted Array. LeetCode162 Find Peak Element
    LeetCode208 Implement Trie (Prefix Tree). LeetCode211 Add and Search Word
    LeetCode172 Factorial Trailing Zeroes. LeetCode258 Add Digits. LeetCode268 Missing Number
    LeetCode191 Number of 1 Bits. LeetCode231 Power of Two. LeetCode342 Power of Four
    LeetCode225 Implement Stack using Queues
    LeetCode150 Evaluate Reverse Polish Notation
    LeetCode125 Valid Palindrome
    LeetCode128 Longest Consecutive Sequence
    LeetCode124 Binary Tree Maximum Path Sum
    LeetCode123 Best Time to Buy and Sell Stock III
  • 原文地址:https://www.cnblogs.com/haogj/p/4473477.html
Copyright © 2020-2023  润新知