jQuery源码:Callbacks

267 阅读6分钟

一、Callbacks概述

1、概述

jQuery中的$.Callbacks用于管理函数队列,是在jQuery内部使用的,如为.ajax$.Deferred等组件提供基础功能的函数。它也可以用在类似功能的一些组件中,如自己开发的插件。

通过调用$.Callbacks可以获取到一个Callbacks实例,有两个方法addfire,通过add添加处理函数到队列中,通过fire去执行这些处理函数。如:

var cb = $.Callbacks();
cb.add(function () {
    console.log('add one');
});
cb.fire();

2、参数

$.Callbacks通过字符串参数的形式,支持4种特定功能:分别是onceuniquestopOnFalsememory

参数once:函数队列只执行一次。

// 不传入参数once
var cb = $.Callbacks();
cb.add(function () {
    console.log('add');
});
cb.fire(); // add
cb.fire(); // add
// 传入参数once
var cb = $.Callbacks('once');
cb.add(function () {
    console.log('add');
});
cb.fire(); // add
cb.fire();

参数unique:往内部队列添加的函数保持唯一,不能重复添加。

// 不传入参数unique
var cb = $.Callbacks();
function demo() {
    console.log('demo');
}
cb.add(demo, demo);
cb.fire(); // demo demo
// 传入参数unique
var cb = $.Callbacks('unique');
function demo() {
    console.log('demo');
}
cb.add(demo, demo);
cb.fire(); // demo

参数stopOnFalse:内部队列里的函数是依次执行的,当某个函数的返回值是false时,停止继续执行剩下的函数。

// 不传入参数stopOnFalse
var cb = $.Callbacks();
cb.add(function(){
    console.log('one');
    return false; // 不添加参数stopOnFalse会执行下一个函数
}, function(){
    console.log('two');
});
cb.fire(); // one two
// 传入参数stopOnFalse
var cb = $.Callbacks('stopOnFalse');
cb.add(function(){
    console.log('one');
    return false; // 添加参数stopOnFalse时不会执行下一个函数
}, function(){
    console.log('two');
});
cb.fire(); // one

参数memory:当函数队列fire一次过后,内部会记录当前fire的参数。当下次调用add时,会把记录的参数传递给新添加的函数并立即执行这个新添加的函数。

// 不传入参数memory
var cb = $.Callbacks();
cb.add(function () {
    console.log('add one');
});
cb.fire(); // add one
// 不添加参数memory时不会执行这个新添加的函数
cb.add(function(){
    console.log('add two');
});
// 传入参数memory
var cb = $.Callbacks('memory');
cb.add(function () {
    console.log('add one');
});
cb.fire(); // add one
// 添加参数memory时会执行这个新添加的函数
cb.add(function(){
    console.log('add two');
}); // add two

3、概念解读

为何会有Callbacks的产生?我们可以从事件函数来了解Callbacks。事件通常与函数配合使用,这样就可以通过发生的事件来驱动函数的执行。

事件函数遵循两个原则:

一个事件对应一个事件函数,即一对一。

在一个事件对应多个事件函数的情况下,后者会覆盖前者。

如:

// 传入参数memory
Element.onclick = function (){
    console.log('onclick1');
}
Element.onclick = function (){
    console.log('onclick2');
}

我们往DOM元素上绑定两次onclick事件,当我们触发该点击事件时,只触发打印onclick2

那能不能一对多呢?可以,下面这个就是一对多的事件模型:

var callbacks = [function() {
    console.log(1);
}, function() {
    console.log(2);
}, function() {
   console.log(3);
}];
Element.onclick = function() {
    var _this = this;
    callbacks.forEach(function(fn) {
        fn.call(_this);
    })
}

把事件存储在callbacks这个数组中,触发onclick事件时通过forEach去遍历,来执行数组里面的函数,这样就建立起了一对多的事件模型了。

回到Callbacks身上,Callbacks并不只是要创建一个数组,把所有的事件函数都丢到里面去,它还有其他更多更灵活的功能:即Add()往容器中添加处理函数,Fire()按照添加函数的顺序依次执行处理函数,不是事件驱动型,而是通过fire去控制事件是否依次执行。

还有另外一个就是参数控制:stopOnFalse可选,执行某个处理函数是,返回值为false,则终止后续处理函数执行;Once 默认,fire调用后关闭容器,add添加进容器的处理函数将不会执行。Memory可选,fire调用后开发容器,add添加进容器的处理函数将会立即执行。

二、Callbacks源码解析

我们以单独的一个功能模块抽离出来,来看一下Callbacks是怎么实现的:

1、处理传入的参数

首先创建一个_对象,在_对象扩展一个Callbacks方法。Callbacks方法接收一个参数optionsoptions理论上是onceuniquestopOnFalsememory这4种中的一种或者多种。

(function (root) {
    var _ = {
        Callbacks: function (options) { // options:接收参数
            console.log(options);
        },
    }
    root._ = _; // 给window扩展一个属性来拿到对象的引用
})(window);

然后定义一个optionsCache对象,该对象用来对传入的参数选项进行缓存。

再定义一个createOptions函数,对传入的可能以空格为分隔符的字符串进行处理,并存储到缓存对象optionsCache对象中。

Callbacks函数中,判断options是否为一个字符串,如果是就从缓存对象optionsCache去获取或者新建一个缓存对象,如果否就新建一个对象。

(function (root) {
    var optionsCache = {};
    var _ = {
        Callbacks: function (options) { // options:接收参数
            console.log(options);
            options = typeof options === 'string' ? (optionsCache[options] || createOptions(options)) : {};
        },
    }
    function createOptions(options) {
        var obj = optionsCache[options] = {};
        // 以空格(\s+)分割参数(支持传入多个参数,以空格分开)
        options.split(/\s+/).forEach(function (value) {
            obj[value] = true;
        });
        // console.log(obj);
        return obj; // 获取到用户传来的参数,支持传入多个参数,并存储在optionsCache缓存对象中
    }
    root._ = _; // 给window扩展一个属性来拿到对象的引用
})(window);

2、添加add和fire方法

add方法的核心是把传入的函数加到一个数组(即函数队列)里面。而fire方法则比较复杂,需要用self.fire来控制参数的传递,用self.fireWith来进行上下文的绑定,方便控制执行过程。下面是代码的实现:

Callbacks: function(options) { // options:接收参数
    // console.log(options);
    options = typeof options === 'string' ? (optionsCache[options] || createOptions(options)) : {};
    // 函数队列,赋予很多的功能
    var list = [];
    // index:执行位置
    var index, length;
    // 真正能够控制执行队列里的处理函数的方法
    var fire = function(data) {
        index = 0;
        length = list.length;
        for (; index < length; index++) {
            list[index].apply(data[0], data[1])
        }
    }
    var self = {
        add: function() {
            var args = [...arguments]; // 把参数转化为真正的数组
            args.forEach(function(fn) {
                // 判断是否是函数,是函数则加进list列表中
                if (toString.call(fn) === '[object Function]') {
                    list.push(fn);
                }
            })
        },
        // 用于上下文的绑定,方便控制执行过程
        fireWith: function(context, arguments) {
            var args = [context, arguments];
            fire(args);
        },
        // 这个fire并不是要依次执行队列里函数的函数,调用时需要往里面传参
        fire: function() {
            self.fireWith(this, arguments); // 控制参数的传递 this:self
        },
    }
    // 每次调用Callbacks,都返回一个队列,队列里面有add、fire这些操作
    return self;
}

3、参数的特定功能实现和完整代码

(function (root) {
    // 以单独的一个功能模块抽离出来,来看一下Callbacks是怎么实现的
    var optionsCache = {};
    var _ = {
        Callbacks: function (options) { // options:接收参数
            // console.log(options);
            options = typeof options === 'string' ? (optionsCache[options] || createOptions(options)) : {};
            // 函数队列,赋予很多的功能
            var list = [];
            // index:执行位置 testting:是否被执行过
            var index, length, testting, memory, start, starts;
            // 真正能够控制执行队列里的处理函数的方法
            var fire = function (data) {
                memory = options.memory && data;
                index = starts || 0;
                start = 0;
                testting = true;
                length = list.length;
                for (; index < length; index++) {
                    if (list[index].apply(data[0], data[1]) === false && options.stopOnFalse) {
                        // 当某个函数的返回值是false时,停止继续执行剩下的函数
                        break;
                    }
                }
            }
            var self = {
                add: function () {
                    var args = [...arguments]; // 把参数转化为真正的数组
                    start = list.length;
                    args.forEach(function (fn) {
                        // 判断是否是函数,是函数则加进list列表中
                        if (toString.call(fn) === '[object Function]') {
                            list.push(fn);
                        }
                    })
                    if (memory) {
                        starts = start;
                        fire(memory);
                    }
                },
                // 用于上下文的绑定,方便控制执行过程
                fireWith: function (context, arguments) {
                    var args = [context, arguments];
                    if (!options.once || !testting) {
                        // once:函数队列只执行一次
                        fire(args);
                    }
                },
                // 这个fire并不是要依次执行队列里函数的函数,调用时需要往里面传参
                fire: function () {
                    self.fireWith(this, arguments); // 控制参数的传递 this:self
                },
            }
            // 每次调用Callbacks,都返回一个队列,队列里面有add、fire这些操作
            return self;
        },
    }
    function createOptions(options) {
        var obj = optionsCache[options] = {};
        // 以空格(\s+)分割参数(支持传入多个参数,以空格分开)
        options.split(/\s+/).forEach(function (value) {
            obj[value] = true;
        });
        // console.log(obj);
        return obj; // 获取到用户传来的参数,支持传入多个参数,并存储在optionsCache缓存对象中
    }
    root._ = _; // 给window扩展一个属性来拿到对象的引用
})(window);
var cb = _.Callbacks();
// var cb = _.Callbacks('once');
// var cb = _.Callbacks('unique:');
// var cb = _.Callbacks('stopOnFalse');
// var cb = _.Callbacks('memory');
cb.add(function() {
    console.log('1');
});
cb.add(function() {
    console.log('2');
});
cb.fire();
cb.add(function() {
    console.log('3');
});