jQuery源码阅读(五):Callback API

Author : lovecicy

上一篇讲述了jQuery Callback API的一个bug,这一篇我们言归正传,来介绍一下Callback API。

Callback API于1.7版本引入,它提供了一种强大的方式来管理一系列的回调函数,可以添加,删除,触发或者停用回调函数。

可能我们在平时使用中不太会用到Callback API,但是在jQuery内部,Callback API可是发挥了很大的作用的。在jQuery的Ajax API和Deferred API中都有用到,而我们的$(calback)方法,依赖于Deferred API,其实也是依赖Callback API的。而且,在平时我们自己写代码的过程中,我们同样可以用Callback API来管理我们的回调函数。

交代完了它的背景,让我们来看看真实的代码吧。

其实在写这篇文章前,我已经看过了前端狮子对于Callback API的解读,虽然我们解读的并非同一个版本的jQuery,但是狮子的文章在一定程度上帮我理解了这部分代码,并且狮子的解读与文章或多或少对我的解读产生了影响,所以,在写这篇文章时,我的内心也在纠结,如果写出类似的文章,那写的意义何在;而如果不写,这系列的解读少了这一部分就不完整了。因此,写是必须的,但是要避免写出一样的文章,或者在狮子的基础上写出更容易让读者理解的文章。因此,这篇文章需要绞尽脑汁,耗费更多的时间,希望这时间没有白费,对读者有些帮助。

一、可选的模式

$.Callbacks()方法会返回一个对象,通过这个对象对回调函数进行操作。这个方法接受一个空格分割的字符串作为参数,提供多种模式,以满足多种功能的Callback对象。下面是可选的模式,四个选项可以任意组合。

/*
 *
 * Possible options:
 *
 *  once:           will ensure the callback list can only be fired once (like a Deferred)
 *                  确保这个callback列表只会被执行一次
 *
 *  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)
 *                  会记住前一次执行时的参数,执行后被添加的callback会被立即执行,并且传入被记住的之前的参数
 *
 *  unique:         will ensure a callback can only be added once (no duplicate in the list)
 *                  确保一个callback只会被添加一次
 *
 *  stopOnFalse:    interrupt callings when a callback returns false
 *                  当某个callback返回false时,终止执行callback列表
 *
 */

once, unique和stopOnFlase很好理解,但是我知道你肯定理解不了为什么要有memory这个模式。但是我给你举个例子你肯定就明白了:$();

当我们刚开始调用$(callback)时,这个回调函数不会被执行,因为我们要等DOM Ready了才回触发callback。当DOM Ready的时候jQuery会触发这个Callback对象,执行回调函数,并给它传递了jQuery作为参数(不信的同学可以在自己的代码里设个断点,看看arguments[0] === $是否为true)。之后我们再写这样的代码$(callback2)的时候,DOM已经Ready了,jQuery不会再触发Callback对象,而应该直接执行callback2,并且同样给她传递jQuery作为参数。看,是不是和memory模式适用的场景一样?当然,我们的代码只能执行一遍,因此,$(callback)中的Callback对象使用的是”once memory”这个模式。

二、那些变量们

在开始看Callback API的方法之前,让我们先来理解下Callback API代码中诸多的变量吧,这些变量可是非常重要的哦。

var // 这个变量就是针对memory模式的,用来保存上次触发时的参数
    memory,

    // 标志位,表示是否被触发过
    fired,

    // 标志位,表示是否正在触发,可以判断是否处于递归触发(这个变量和stack变量很有关系)
    firing,

    // 只在内部使用,标明首个要被触发的回调函数的下标
    firingStart,

    // 表示回调函数列表的长度
    firingLength,

    // 表示正在执行的回调函数的下标
    firingIndex,

    // 回调函数列表
    list = [],

    // 顾名思义,这个就是栈啦,什么用呢?当我们在执行一个回调函数的时候,
    // 在函数内部又触发了这个Callback对象,就用到这个stack啦。当然,如果是once模式,
    // 就根本不存在在某个回调函数里面再次触发这个Callback对象的情况啦,
    // 因此判断options.once是否为true。具体的用处等到下一节再讲
    stack = !options.once && []

其实就两个变量的意义比较难理解:’memory’和’stack’,特别是’stack’,我们先知道他的用处是什么,再来看代码里究竟怎么使用它,Let’s GO!

三、代码解析

这一节主要讲述Callback对象的方法,一些简单的就略过啦。

// 缓存options对象
var optionsCache = {};

// 把字符串格式的options转换为对象
function createOptions( options ) {
    ......
    return object;
}

jQuery.Callbacks = function( options ) {

    // 将options转换成对象
    options = typeof options === "string" ?
        ( optionsCache[ options ] || createOptions( options ) ) :
        jQuery.extend( {}, options );

    var memory,fired,firing,firingStart,firingLength,
        firingIndex,list = [],stack = !options.once && [],
        // 这里真正的触发回调函数,可以先不看这段,先看self里的方法吧
        fire = function( data ) {
            // 最后来看看fire()方法吧
            // 如果是'memory'模式,那么memory就不再是undefined啦
            memory = options.memory && data;
            // 这里将fired置为true,表示已经执行过回调啦
            fired = true;
            // 如果是'memory'模式,firingStart可能不为0,即只执行当前被add()的回调
            firingIndex = firingStart || 0;
            firingStart = 0;
            firingLength = list.length;
            // 这里将firing置为true,是用于在回调函数内调用self.fire()的情况,下一节将展示
            firing = true;
            // 遍历list,执行所有的回调函数
            for ( ; list && firingIndex < firingLength; firingIndex++ ) {
                // 这里用到了apply方法,将回调函数的this指向self.fireWith()传入的上下文
                // 并且检测在'stopOnFalse'模式中判断是否回调函数返回了false
                if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false 
                         && options.stopOnFalse ) {
                    // 这里的作用是方式'memory stopOnFalse *'模式下后add()的回调再被执行
                    memory = false; // To prevent further calls using add
                    break;
                }
            }
            // 结束调用回调,将firing置为false
            firing = false;
            if ( list ) {
                if ( stack ) {
                    if ( stack.length ) {          // -----------标记D-----------
                        fire( stack.shift() );
                    }
                // stack为undefined可能的情况:
                // 1. 'once'
                // 2. 'memory'模式下lock
                // 3. disable掉了
                } else if ( memory ) {
                // 第二种情况:'memory'模式下lock后再add()
                    list = [];
                } else {
                // 另外两种情况就disable掉吧,打完收工啦
                    self.disable();
                }
            }
        },
        // 返回的Callback对象
        self = {
            // 往list中添加一个回调函数
            add: function() {
                // 先判断list是否存在
                if ( list ) {
                    // 首先,保存当前回调列表的长度
                    // 用于memory模式
                    var start = list.length;
                    
                    // 这里,jQuery用了一个自执行函数来添加回调函数
                    (function add( args ) {
                        // 对传入add()的参数进行遍历
                        jQuery.each( args, function( _, arg ) {
                            var type = jQuery.type( arg );
                            // 如果是function,并且检查是否为unique模式
                            // 若为unique模式,则确保当前回调函数不存在
                            if ( type === "function" && 
                                     ( !options.unique || !self.has( arg ) ) ) {
                                list.push( arg );
                            } else if ( arg && arg.length && type !== "string" ) {
                                // 如果是数组或类数组对象,则递归调用add()
                                add( arg );
                            }
                        });
                    })( arguments );
                    
                    // 按理说添加回调函数的工作已经完成了,但还有需要特殊处理的场景:
                    // 如果add()方法是在某个回调函数中被调用,
                    // 则需要将当前添加的回调函数添加到本次触发的队列里
                    if ( firing ) {
                        firingLength = list.length;

                    // 如果是memory模式,并且memory变量有值,
                    // 则立即触发刚添加的回调函数
                    } else if ( memory ) {
                        // ------------标记A----------------
                        firingStart = start;
                        fire( memory );
                    }
                }
                return this;
            },
            // 从list中删除回调函数
            remove: function() {
                if ( list ) {
                    ......
                }
                return this;
            },
            // 判断回调函数是否已存在
            has: function( fn ) {
                return jQuery.inArray( fn, list ) > -1;
            },
            // 清空回调函数列表
            empty: function() {
                // 这个if判断是我加的,原因在上一篇文章已经交待了
                if(list){
                    // empty的作用就是清空所有回调函数啦,
                    // 所以将list置为空数组即可
                    list = [];
                }
                return this;
            },
            // 禁用当前Callback对象,这里将三个变量都置为undefined,
            // 禁用后所有对此Callback对象的操作都应该被禁止。
            // 注意这三个变量对Callback API的方法很重要,
            // 很多方法都需要对这些变量进行判断来确定是否继续执行,
            // 以下方法都需要对list进行判断:
            // add(),remove(),fireWith(),fire(),empty(),disabled()
            // 而locked()方法则需要对stack进行判断。
            disable: function() {
                list = stack = memory = undefined;
                return this;
            },
            disabled: function() {return !list;},
            // 官方文档给的描述是:Lock a callback list in its current state.
            // 但是什么叫锁定呢?其实和disable()的作用一样,禁用这个Callback对象
            // 有一个例外,就是当前模式是'memory'模式,并且fire()过了,
            // 这时候,add()方法还能用,fire()就失效了
            lock: function() {
                stack = undefined;
                // 这句话是关键,对memory的判断,如果memory为false,则调用disable(),
                // 这个时候lock()和disable()方法没区别
                // 当memory为true时,不调用disable()方法,也就是list和memory不为undefined
                // 而memory不为undefined的前提是'memory'模式并且已经调用过self.fire(args)
                // 这时,list和memory不会被置为undefined,因此add方法仍然可用
                // 不过要再调用self.fire()时,因为fired为true,且stack为undefined,所以无法执行回调。
                // 而调用add()方法时,判断的是list,list不为undefined,所以可以添加成功。
                // 同时因为memory不为undefined,上面的标记A处代码得以执行,立即执行新添加的回调。
                if ( !memory ) {
                    self.disable();
                }
                return this;
            },
            locked: function() {return !stack;},

            // 执行所有的回调函数
            // 第一个参数是回调函数的上下文对象,即回调函数中的this
            fireWith: function( context, args ) {
                args = args || [];
                // 将上下文对象也放入参数列表中
                args = [ context, args.slice ? args.slice() : args ];
                // 判断是否需要执行回调函数,哪些情况不能再执行回调函数呢:
                // 1. 被lock或disable掉了
                // 2. once模式,并且已经执行过一次了
                // 第一种情况,只需判断list是否为undefined
                // 第二种情况,则判断是否执行过了,若没执行过,则执行;
                // 若执行过了,则只要不为'once'模式即可,而如果是'once'模式,
                // 则stack为undefined。
                if ( list && ( !fired || stack ) ) {
                    // 这里判断firing是因为有可能在回调函数中,有可能再次调用self.fire()函数
                    // 这时,jQuery选择先将外层的调用先执行完,将回调函数中调用的self.fire()
                    // 的参数放入stack中,等待外层的调用执行完毕,再执行内层的self.fire()调用
                    // -------------标记B----------------
                    if ( firing ) {
                        stack.push( args );
                    } else {
                    // 正常执行回调函数
                    // 接下来再去看上面的fire()方法吧
                        fire( args );
                    }
                }
                return this;
            },
            // 这里还是调用上面的fireWith()方法,只是context被置为当前的Callback对象
            fire: function() {
                self.fireWith( this, arguments );
                return this;
            },
            fired: function() {return !!fired;}
        };

    return self;
};

四、API使用

如果对上面的代码解释还有疑惑的地方,比如list,stack,memory或者firing变量,那么,通过下面的API使用,再对照Callback API的代码,相信你的疑惑一定会被解开了。

上面的标记B处适用于下面的情况

var cb = $.Callbacks();
var flag = false;
var a = function(){
    console.log('a');
};
var b = function(){
    console.log('b:');
    console.log(arguments.length);
    if(!flag){
        cb.fire({'test':'testing'});    // -------------标记C--------------
        flag = true;
    }
};
var c = function(){
    console.log('c');
};
cb.add(a);
cb.add(b);
cb.add(c);
cb.fire();

上面的代码执行时,先输出’a’,再输出’b:0’,接下来执行标记C处的代码,这时进入jQuery代码中的标记B处,此时firing为true,因此将当前的{'test':'testing'}对象放入stack中,然后退出,再执行下一个回调函数,打印出’c’,接着,会执行标记D处的代码,再执行一遍所有的回调函数,并将{'test':'testing'}对象作为参数传给所有的回调函数,最终输出:’ab:1c’。如果还没明白,那就再好好琢磨琢磨吧(^o^)/~

友情支持:前端狮子的Callback API解析

standard

Have your say