jQuery Callbacks

Posted by franki on June 6, 2019

jQuery Callbacks 是一个多用途的回调函数列表对象,提供一个强大的方法来管理回调函数队列

常见处理队列的的方法

etc

function dowithList (List, cb) {
  setTimeout(function() {
    var task = List.shift();
    task();
    if (List.length > 0) {
      setTimeout(function() {
        dowithList(List, cb);
      }, 1000);
    } else {
      cb && cb();
    }
  }, 25);
}

dowithList([
  function a() {console.log('a');},
  function b() {console.log('b');},
  function c() {console.log('c');}
]);

jQuery提供给我们的方式:

var callbacks = $.Callbacks();
var a = function() {
  console.log('a');
};
var b = function() {
  console.log('b');
};
callbacks.add(a);
callbacks.add(b);
callbacks.fire();
// outputs: a b

上面的执行,简单明了,jQuery提供了一个强大的回调列表对象,来管理回调函数队列。

整体思想依靠pub/sub(发布订阅的模式)来管理回调函数队列的添加与触发执行的操作。

// 最简单的pub/sub
var Observer = {
  list: [],
  sub: function(fn) {
    this.list.push(fn);
  },
  pub: function(value) {
    for (var i=0; i<this.list.length; i++) {
      this.list[i](value);
    }
  }
}

// test
var a = function(value) {
  console.log('a: ', value);
}
Observer.sub(a);
Observer.pub(1);
// outputs: a: 1

jQuery 里面Callbacks实现的代码也很少,主要以下代码:

jQuery.Callbacks = function(optisons) {
  // Convert options from String-formatted to Object-formatted if needed
  // (we check in cache first)
  options = typeof options === "string" ?
    createOptions( options ) :
    jQuery.extend( {}, options );
  
    var // Flag to know if list is currently firing
      firing,

      // Last fire value for non-forgettable lists
      memory,

      // Flag to know if list was already fired
      fired,

      // Flag to prevent firing
      locked,

      // Actual callback list
      list = [],

      // Queue of execution data for repeatable lists
      queue = [],

      // Index of currently firing callback (modified by add/remove as needed)
      firingIndex = -1,

      // Fire callbacks
      fire = function() {

      },
      self = {
      ...
      };
  return self;
}

jQuery.Callbacks返回了一个对象,里面封装回调对象队列一系列的处理逻辑。总结下主要是以下方法

add(fn | arr) // 添加函数到回调队列
fire(value) // 触发回调队列函数的执行的触发函数
remove(fn) // 从回调队列中去掉某条数据
has(fn) // 当前数据是否存在于回调队列
empty() // 清空回调队列
disable() // 禁止触发.fire 和 .add方法,并且清空所有的callbacks和值
lock() // 锁住.fire方法,忽略正在构建的执行
locked() // 判断最近的一次是否执行了所有的回调函数

从add 到 fire 主要经过这些方法调用

jq-add-to-fire

self上的add方法可以单独拿来说说

add: function() {
  if ( list ) {

    // If we have memory from a past run, we should fire after adding
    if ( memory && !firing ) {
      firingIndex = list.length - 1;
      queue.push( memory );
    }

    ( function add( args ) {
      jQuery.each( args, function( _, arg ) {
        if ( isFunction( arg ) ) {
          if ( !options.unique || !self.has( arg ) ) {
            list.push( arg );
          }
        } else if ( arg && arg.length && toType( arg ) !== "string" ) {

          // Inspect recursively
          add( arg );
        }
      } );
    } )( arguments );

    if ( memory && !firing ) {
      fire();
    }
  }
  return this;
}

现在对上面的代码稍微进行说明,里面有个立即执行的函数,作用是接收外面传入的arguemtns,可以接收单个函数,也可以是函数数组,把接收的值push入list回调队列。

下面继续分析fire方法

// 首先是self 的 fire
// Call all the callbacks with the given arguments
var self = {
  // Call all callbacks with the given context and arguments
  fireWith: function( context, args ) {
    if ( !locked ) {
      args = args || [];
      args = [ context, args.slice ? args.slice() : args ];
      queue.push( args );
      if ( !firing ) {
        fire();
      }
    }
    return this;
  },
  fire: function() {
    self.fireWith( this, arguments );
    return this;
  },
}

// Callbacks内定义的fire函数
// Fire callbacks
fire = function() {

  // Enforce single-firing
  locked = locked || options.once;

  // Execute callbacks for all pending executions,
  // respecting firingIndex overrides and runtime changes
  fired = firing = true;
  for ( ; queue.length; firingIndex = -1 ) {
    memory = queue.shift();
    while ( ++firingIndex < list.length ) {

      // Run callback and check for early termination
      if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false &&
          options.stopOnFalse ) {

        // Jump to end and forget the data so .add doesn't re-fire
        firingIndex = list.length;
        memory = false;
      }
    }
  }

  // Forget the data if we're done with it
  if ( !options.memory ) {
    memory = false;
  }

  firing = false;

  // Clean up if we're done firing for good
  if ( locked ) {

    // Keep an empty list if we have data for future add calls
    if ( memory ) {
      list = [];

      // Otherwise, this object is spent
    } else {
      list = "";
    }
  }
},

触发过程为: self.fire-self.fireWith-fire

fireWith的作用是接收参数,放入queue,这个queue在外层fire中会使用到,主要是给list回调队列里的函数作为参数传递出去,这样之前add进来的函数,就可以触发了,触发的动作是:

// Run callback and check for early termination
if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false &&
options.stopOnFalse ) {

  // Jump to end and forget the data so .add doesn't re-fire
  firingIndex = list.length;
  memory = false;
}

$.Callbacks(‘once’)

确保这个回调队列的函数只执行一次

var foo = function(value) {
  console.log('foo: ' + value);
};

// another function to also be added to the list
var bar = function(value) {
  console.log('bar: ' + value);
};

var callbacks = $.Callbacks('once');
// add the function "foo" to the list
callbacks.add([foo, bar]);

// fire the items on the list
callbacks.fire('hello');
callbacks.fire('world');
// outputs foo: hello bar: hello 后面的fire不执行了

改变这个行为是由于

// Callbacks的fire方法,配置了once参数,会设置optins.once = true
// Enforce single-firing
locked = locked || options.once;

// 第一次fire完,会检查是否有锁住的情况
// Clean up if we're done firing for good
if ( locked ) {

  // Keep an empty list if we have data for future add calls
  if ( memory ) {
    list = [];

    // Otherwise, this object is spent
  } else {
    list = "";
  }
}
// list 回调队列会被重置
// 后续再调用fire就不会有输出了

$.Callbacks(‘unique’) 确保一次只能添加一个回调

var foo = function(value) {
  console.log('foo: ' + value);
};

var callbacks = $.Callbacks();
callbacks.add([foo, foo]);
callbacks.fire('hello');
// outputs foo hello 只输出一次

下面附上Callbacks的源码:

jQuery.Callbacks = function( options ) {

    // Convert options from String-formatted to Object-formatted if needed
    // (we check in cache first)
    options = typeof options === "string" ?
        createOptions( options ) :
        jQuery.extend( {}, options );

    var // Flag to know if list is currently firing
        firing,

        // Last fire value for non-forgettable lists
        memory,

        // Flag to know if list was already fired
        fired,

        // Flag to prevent firing
        locked,

        // Actual callback list
        list = [],

        // Queue of execution data for repeatable lists
        queue = [],

        // Index of currently firing callback (modified by add/remove as needed)
        firingIndex = -1,

        // Fire callbacks
        fire = function() {

            // Enforce single-firing
            locked = locked || options.once;

            // Execute callbacks for all pending executions,
            // respecting firingIndex overrides and runtime changes
            fired = firing = true;
            for ( ; queue.length; firingIndex = -1 ) {
                memory = queue.shift();
                while ( ++firingIndex < list.length ) {

                    // Run callback and check for early termination
                    if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false &&
                        options.stopOnFalse ) {

                        // Jump to end and forget the data so .add doesn't re-fire
                        firingIndex = list.length;
                        memory = false;
                    }
                }
            }

            // Forget the data if we're done with it
            if ( !options.memory ) {
                memory = false;
            }

            firing = false;

            // Clean up if we're done firing for good
            if ( locked ) {

                // Keep an empty list if we have data for future add calls
                if ( memory ) {
                    list = [];

                // Otherwise, this object is spent
                } else {
                    list = "";
                }
            }
        },

        // Actual Callbacks object
        self = {

            // Add a callback or a collection of callbacks to the list
            add: function() {
                if ( list ) {

                    // If we have memory from a past run, we should fire after adding
                    if ( memory && !firing ) {
                        firingIndex = list.length - 1;
                        queue.push( memory );
                    }

                    ( function add( args ) {
                        jQuery.each( args, function( _, arg ) {
                            if ( isFunction( arg ) ) {
                                if ( !options.unique || !self.has( arg ) ) {
                                    list.push( arg );
                                }
                            } else if ( arg && arg.length && toType( arg ) !== "string" ) {

                                // Inspect recursively
                                add( arg );
                            }
                        } );
                    } )( arguments );

                    if ( memory && !firing ) {
                        fire();
                    }
                }
                return this;
            },

            // Remove a callback from the list
            remove: function() {
                jQuery.each( arguments, function( _, arg ) {
                    var index;
                    while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {
                        list.splice( index, 1 );

                        // Handle firing indexes
                        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.length > 0;
            },

            // Remove all callbacks from the list
            empty: function() {
                if ( list ) {
                    list = [];
                }
                return this;
            },

            // Disable .fire and .add
            // Abort any current/pending executions
            // Clear all callbacks and values
            disable: function() {
                locked = queue = [];
                list = memory = "";
                return this;
            },

            disabled: function() {
                return !list;
            },

            // Disable .fire
            // Also disable .add unless we have memory (since it would have no effect)
            // Abort any pending executions
            lock: function() {
                locked = queue = [];
                if ( !memory && !firing ) {
                    list = memory = "";
                }
                return this;
            },

            locked: function() {
                return !!locked;
            },

            // Call all callbacks with the given context and arguments
            fireWith: function( context, args ) {
                if ( !locked ) {
                    args = args || [];
                    args = [ context, args.slice ? args.slice() : args ];
                    queue.push( args );
                    if ( !firing ) {
                        fire();
                    }
                }
                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;
};

总结:jQuery的Callbacks其实很简洁,只有不到200行代码,核心就是通过观察者模式管理回调队列,建立了一套程序的通信机制。