• 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;
    };
  • 相关阅读:
    arm activesync 串口(wince移植篇)
    请问生产成本收集器与标准成本评估有什么关联?
    BAPI / RFC with Delphi(系列之六)--TSAPFunctions使用BAPI创建PO(有登录对话框的delphi源代码)
    关于项目团队的发展阶段特点、问题、应对措施、领导风格
    如何在sap里设置打印机参数
    BAPI / RFC with Delphi(系列之四)--TSAPFunctions使用(有登录对话框的delphi源代码)
    BAPI / RFC with Delphi(系列之七)--TBAPIControl使用BUS1001显示物料(Delphi源代码)
    BAPI / RFC with Delphi(系列之五)--TSAPFunctions和TSAPLogoncontrol使用(无登录对话框Delphi源代码)
    Guice 1.0 用户指南
    BAPI / RFC with Delphi(系列之八)--TBAPIControl使用BUS2012建立PO(Delphi源代码)
  • 原文地址:https://www.cnblogs.com/haogj/p/4473477.html
Copyright © 2020-2023  润新知