w3ctech

jQuery源码剖析(二)——$.Callbacks

前言

上一篇《jQuery源码剖析(一)——概览&工具方法》最后说要了解一下jQuery的ready实现。 为了彻底理解jQuery的ready实现,所以需要理解什么是异步队列。 为了彻底理解jQuery的异步队列实现,所以需要理解什么是回调函数队列管理器(姑且叫这个名字吧,其实就是jQuery.Callbacks)。 可以看到学习实际上就是一个拓扑的问题,这篇文章完全是剖析$.Callbacks的原理&实现。 官方文档上写到$.Callbacks的用途:

A multi-purpose callbacks list object that provides a powerful way to manage callback lists. $.Callbacks是在1.7的版本之后新增的,它提供了一些很优美的方法来管理回调函数队列。

回调函数队列

对于javascript来说,回调这个概念太常见了。

  • setTimeout(callback, 3000):3秒后触发回调callback函数
  • $.ajax({success:callback}):ajax成功接收到请求后触发回调callback函数
  • :点击这个div的时候触发callback函数 当比较复杂的场景出现时,一个事件触发时(对应上边例子的就是:3秒到了;ajax成功返回了;div被点击了;)需要同时执行多个回调。 很容易想到的方案就是,我们用一个队列把这个事件对应的回调函数一个一个存起来,事件触发的时候,按照队列的FIFO(先进先出)原则依次执行这些回调函数。 抽象出来之后就如同是以下这样的类:

class Callbacks{
  private list = [];//内部存储回调的队列
  public function add(fn){//添加多一个回调
    list.add(fn)
  };
  public function remove(fn){//移除某个回调
    list.remove(fn)
  };
  public function fire(){//触发事件,执行整条队列
    for(fn in list){
      fn()
    }
  };
}

简单的场景就像上边这样三个接口就可以解决。

$.Callbacks的用例

大概了解什么是回调函数队列是什么之后,回过头看看$.Callbacks的一些用法。 首先有如下两个回调函数


function fn1(arg) {//回调一
    console.log("fn1 : " + arg);
}
function fn2(arg) {//回调二
    console.log("fn2 : " + arg);
}

生成一个Callbacks实例,用add操作添加回调,然后用fire触发事件


var callbacks = $.Callbacks();//可以看到这里的$.Callbacks是一个工厂方法,用于生成实例
callbacks.add(fn1);

//输出 => fn1 : Alice
callbacks.fire("Alice");

callbacks.add(fn2);//添加多一个回调

//输出 => fn1 : Bob, fn2 : Bob
callbacks.fire("Bob");

用remove接口来移除一个回调。


callbacks.remove(fn1);//删除回调一

//输出 => fn2 : Bob
callbacks.fire("Bob");

更复杂的场景,某个场景要求只能触发一次事件,之后便不再触发。


var callbacks = $.Callbacks("once");//只触发一次事件的管理器
callbacks.add(fn1);
callbacks.fire("Alice");//输出 => fn1 : Alice
callbacks.add(fn2);
callbacks.fire("Bob");//触发不再生效了

从以上的例子可以看到,Callbacks这个工厂方法接受参数,可以生成不同类型的管理器。 总共有以下四种子类型的管理器:

  • once: 只触发一次回调队列(jQuery的异步队列使用的就是这种类型)
  • memory: 这个解释起来有点绕,用场景来描述就是,当事件触发后,之后add进来的回调就直接执行了,无需再触发多一次(jQuery的异步队列使用的就是这种类型)
  • unique: 队列里边没有重复的回调
  • stopOnFalse: 当有一个回调返回是false的时候中断掉触发动作 以上4种类型是可以结合使用的,用空格分隔开传入工厂方法即可:$.Callbacks("once memory unique") 在异步队列(有三种状态:已完成|已失败|未完成)里边,用了三个管理器:

  • done = jQuery.Callbacks("once memory");//已完成

  • fail = jQuery.Callbacks("once memory")//已失败
  • progress = jQuery.Callbacks("memory")//未完成 $.Callbacks生成的管理器实例提供了以下接口(官方文档)。

`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都不生效

callbacks.has(fn)//判断有无fn回调在队列里边 callbacks.empty()//清空回调队列 callbacks.disabled()//管理器是否禁用 callbacks.fired()//是否已经触发过,即是有没有fire/fireWith过 callbacks.locked()//判断是否锁住队列 `

$.Callbacks源码

$.Callbacks在源码包里边的callbacks.js文件,总共是196行。 以下是Callbacks的源码的大概结构:



jQuery.Callbacks = function( options ) {
  var options = createOptions(options);

  var 
    memory,
    fired,
    firing,
    firingStart,
    firingLength,
    firingIndex,
    list = [],
    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;
};

这里的回调函数管理器是可以并发的,也就是在触发事件的过程当中有可能有其他回调被加入队列或者移出队列。 用以下几个示意图来说明一下这个过程: 图解过后就是完整的Callbacks源码注释了:



// 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.split( core_rspace ), function( _, flag ) {
    object[ flag ] = true;
  });
  return object;
}

jQuery.Callbacks = function( options ) {

  // Convert options from String-formatted to Object-formatted if needed
  // (we check in cache first)
  //可以传递字符串:"once memory"
  //也可以传递对象:{once:true, memory:true}
  //这里还用optionsCache[ options ]缓存住配置对象
  //生成的配置对象就是{once:true, memory:true}
  options = typeof options === "string" ?
    ( optionsCache[ options ] || createOptions( options ) ) :
    jQuery.extend( {}, options );

  var 
    // Last fire value (for non-forgettable lists)
    //是否为memory类型的管理器
    memory,

    //fire相关==============================================
    // 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)
    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会keep住fire所需的上下文跟参数(假设称为事件)
    stack = !options.once && [],

    // Fire callbacks
    //触发事件
    //这个函数是内部使用的辅助函数
    //它被self.fire以及self.fireWith调用
    fire = function( data ) {
      //如果是memory类型管理器
      //要记住fire的事件data,以便下次add的时候可以重新fire这个事件
      //看add源码最后一段就知道
      memory = options.memory && data;

      fired = true;
      firingIndex = firingStart || 0;//如果设置了开始位置firingStart
      firingStart = 0;

      //思考一下为什么要firingLength呢
      //其实是因为在fire的时候还有可能在add回调,所以要维护这里的队列长度
      //有点像是为了保证并发
      firingLength = list.length;

      //开始fire============================
      firing = true;
      for ( ; list && firingIndex < firingLength; firingIndex++ ) {
        //data[ 0 ]是函数执行的上下文,也就是平时的this
        //如果是stopOnFalse管理器,并且回调返回值是false,中断!
        if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) {
          //阻止继续add操作,可以看add最后一段源码
          //也就是说只要有一个返回为false的回调
          //后边添加的add都应该被中断掉
          memory = false; // To prevent further calls using add
          break;
        }
      }
      firing = false;
      //结束fire===========================

      if ( list ) {
        if ( stack ) {
          //如果事件栈还不为空
          //不是 "once" 的情况。
          if ( stack.length ) {
            //从栈中拿出一个事件
            //继续fire队列
            fire( stack.shift() );
            //这里是深度遍历,直到事件队列为空
          }
        }//深度遍历结束!

        //等到fire完所有的事件后
        //如果是memory类型管理器,下次还能继续
        else if ( memory ) {
          //清空队列
          //"once memory" ,或者 "memory" 情况下 lock 过。
          list = [];
        } else {//once
          self.disable();
        }
      }
    },

    // Actual Callbacks object
    //工厂生成的管理器实例
    //其实我觉得每次都生成一个self对象,效率不高
    //内部应该封装一个Function,在Function的prototype上绑定add等方法
    //这样每个方法仅会有一个实例,提高内存使用率
    self = {
      // Add a callback or a collection of callbacks to the list
      //添加一个回调push进队列
      add: function() {
        if ( list ) {
          // First, we save the current length
          //首先,保存当前队列的长度
          var start = list.length;

          //这里是将add参数里边是函数的全部入队列
          (function add( args ) {
            jQuery.each( args, function( _, arg ) {
              var type = jQuery.type( arg );
              if ( type === "function" ) {
                if ( !options.unique || !self.has( arg ) ) {//不是unique管理器或者当前队列还没有该回调
                  //将回调push入队列
                  list.push( arg );
                }
              } else if ( arg && arg.length && type !== "string" ) {
                // Inspect recursively
                //因为可以同时add多个回调,arg = [fn1, fn2m [fn3, fn4]]
                //同时这里排除掉type为string的情况,其实是提高效率,不加判断也能正确,
                //递归调用自己,注意这个使用技巧
                add( arg );
              }
            });
          })( arguments );

          // Do we need to add the callbacks to the
          // current firing batch?
          if ( firing ) {//如果在firing当中,那就把需要firing的长度设置成列表长度
            firingLength = list.length;
          // With memory, if we're not firing then
          // we should call right away
          } else if ( memory ) {
            //如果已经fire过并且是memory类型的管理器
            //这里感谢小金改正:memory 在这里是上一次 fire 的 [context, args]
            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 ) {

                //移除的元素如果是之前fire的,把firingLength减一
                if ( index <= firingLength ) {
                  firingLength--;
                }

                //如果移除的元素比当前在firing的索引要小
                //所以firingIndex也要退一步回前边一格
                if ( index <= firingIndex ) {
                  firingIndex--;
                }
              }
            }
          });
        }
        return this;
      },

      // Control if a given callback is in the list
      //内部调用inArray来判断某个回调是否在队列里边
      has: function( fn ) {
        return jQuery.inArray( fn, list ) > -1;
      },

      // Remove all callbacks from the list
      //清空队列
      empty: function() {
        list = [];
        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住当前状态
      //可以从这里看出,只有memory类型才有可能继续fire
      lock: function() {
        stack = undefined;
        if ( !memory ) {
          self.disable();
        }
        return this;
      },

      // Is it locked?
      //判断是否lock住
      locked: function() {
        return !stack;
      },
      // Call all callbacks with the given context and arguments
      fireWith: function( context, args ) {
        args = args || [];
        //把args组织成 [context, [arg1, arg2, arg3, ...]]可以看到第一个参数是上下文
        args = [ context, args.slice ? args.slice() : args ];

        //list不为空(这里我认为应该调用disabled函数比较合理)。
       //并且:没有fire过   或者    stack不为空
        if ( list && ( !fired || stack ) ) {
          if ( firing ) {//如果当前还在firing
            //压入处理栈
            stack.push( args );
          } else {//如果已经fire过,直接就fire了
            fire( args );
          }
        }
        return this;
      },
      // Call all the callbacks with the given arguments
      //可以看到fire跟fireWith是一样的,只是context是当前管理器实例而已
      fire: function() {
        self.fireWith( this, arguments );
        return this;
      },

      // To know if the callbacks have already been called at least once
      //是否已经fire过
      fired: function() {
        return !!fired;
      }
    };

  return self;
};

从$.Callbacks的源码中看到一些处理并发异步的做法,受益匪浅。 下一篇当然是会剖析一下jQuery异步队列的实现。

w3ctech微信

扫码关注w3ctech微信公众号

共收到2条回复

  • > 作者的markdown是不是格式写的不对,我看你两篇jQuery源码解析文章,js代码格式全乱了,看别人的就OK的 > > 别人的

    回复此楼
  • @hjxenjoy 已经更正了。哈哈

    回复此楼