jQuery 之 Callback 实现

来源:互联网 发布:nc数据下载 编辑:程序博客网 时间:2024/05/30 07:14

文章转自:http://www.cnblogs.com/haogj/p/4473477.html

在 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 cachevar optionsCache = {};// Convert String-formatted options into Object-formatted ones and store in cachefunction 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 firedfired,// Flag to know if list is currently firingfiring,// First callback to fire (used internally by add and fireWith)firingStart,// End of the loop when firingfiringLength,// Index of currently firing callback (modified by remove if needed)firingIndex,// Actual callback listlist = [],
复制代码

如果在 fire 处理过程中,某个函数又调用了 fire 来触发事件呢?

我们可以将这个嵌套的事件先保存起来,等到当前的回调序列处理完成之后,再检查被保存的事件,继续完成处理。显然,使用队列是处理这种状况的理想数据结构,如果遇到这种状况,我们就将事件数据入队,待处理的时候,依次出队数据进行处理。什么时候需要这种处理呢?显然不是在 once 的状况下。在 JavaScript 中,堆队列也是通过数组来实现的,push 用来将数据追加到数组的最后,而 shift 用来出队,从数组的最前面获取数据。

不过,jQuery 没有称为队列,而是称为了 stack.

// Stack of fire calls for repeatable listsstack = !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 listremove: 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 listempty: function() {    list = [];    firingLength = 0;    return this;},// Have the list do nothing anymoredisable: function() {    list = stack = memory = undefined;    return this;},// Is it disabled?disabled: function() {    return !list;},
复制代码

锁住的意思其实是不允许再触发事件,stack 本身也用来表示是否禁止再触发事件。所以,通过直接将 stack 设置为 undefined,就关闭了再次触发事件的可能。

复制代码
// Lock the list in its current statelock: 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 argumentsfireWith: 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 argumentsfire: function() {    self.fireWith( this, arguments );    return this;},// To know if the callbacks have already been called at least oncefired: function() {    return !!fired;}};
复制代码

真正的 fire  函数。

复制代码
// Fire callbacksfire = 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;};
复制代码