一、Callbacks概述
1、概述
jQuery中的$.Callbacks用于管理函数队列,是在jQuery内部使用的,如为.ajax,$.Deferred等组件提供基础功能的函数。它也可以用在类似功能的一些组件中,如自己开发的插件。
通过调用$.Callbacks可以获取到一个Callbacks实例,有两个方法add和fire,通过add添加处理函数到队列中,通过fire去执行这些处理函数。如:
var cb = $.Callbacks();
cb.add(function () {
console.log('add one');
});
cb.fire();
2、参数
$.Callbacks通过字符串参数的形式,支持4种特定功能:分别是once、unique、stopOnFalse、memory。
参数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方法接收一个参数options,options理论上是once、unique、stopOnFalse、memory这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');
});