分析jQuery.Callbacks代码时候,知道它的作用是返回一个用于操作callback函数列表的对象,包括添加、删除、触发执行等基本操作。不就是一个数组(只存储了函数的数组)吗?不就是增、删、然后按顺序遍历(执行函数)吗?仅此而矣,可是为什么jQuery.Callbacks的实现写了这么多“冗余”代码,那了那么if-else、那么多表示状态的变量呢?
jQuery.Callbacks = function (flags) {
flags = flags ? (flagsCache[flags] || createFlags(flags)) : {};
var list = [],
stack = [], //当fire正在执行时,有新的fire调用时,将新的fire调用参数暂存在stack中,等当前fire执行完,再以stack中的参数触发fire执行
memory, //用于记录正在执行或已执行fire的参数
firing, //当前fire函数的执行状态
//list中开始执行的位置,初始时是从0开始,但是fire执行结束时,继续向list中添加callback函数时,
// 就要直接用上次的触发fire函数的参数立即执行此添加callback函数,此时从新加入的callback函数开始执行,但是此函数在list的位置需要此参数记录。
firingStart, // 循环开始的位置
firingLength, // list的循环长度
firingIndex, // 循环的当前索引值
add = function (args) {
var i,
length,
elem,
type,
actual;
for (i = 0, length = args.length; i < length; i++) {
elem = args[i];
type = jQuery.type(elem);
if (type === "array") {
add(elem);
} else if (type === "function") {
if (!flags.unique || !self.has(elem)) {
list.push(elem);
}
}
}
},
// Fire callbacks
fire = function (context, args) {
args = args || [];
memory = !flags.memory || [context, args];
firing = true;
firingIndex = firingStart || 0; // 当fire已执行结束,后面添加callback想要从上次结束的位置开始再次执行时 firingStart不再是0了
firingStart = 0;
firingLength = list.length;
for (; list && firingIndex < firingLength; firingIndex++) {
if (list[firingIndex].apply(context, args) === false && flags.stopOnFalse) {
memory = true; // Mark as halted
break;
}
}
firing = false;
if (list) {
if (!flags.once) {//允许多次fire且正在执行时有新的fire等待执行,stack存放等待执行的fire参数
if (stack && stack.length) {
memory = stack.shift();
self.fireWith(memory[0], memory[1]);
}
} else if (memory === true) {
self.disable();
} else {
list = [];
}
}
},
// Actual Callbacks object
self = {
add: function () {
if (list) {
var length = list.length;
add(arguments);
if (firing) {
firingLength = list.length;
} else if (memory && memory !== true) {
firingStart = length;
fire(memory[0], memory[1]);
}
}
return this;
},
// Remove a callback from the list
remove: function () {
if (list) {
var args = arguments, argIndex = 0, argLength = args.length;
for (; argIndex < argLength ; argIndex++) {
for (var i = 0; i < list.length; i++) {
if (args[argIndex] === list[i]) {
// Handle firingIndex and firingLength
if (firing) {
if (i <= firingLength) {
firingLength--;
if (i <= firingIndex) {
firingIndex--;
}
}
}
// Remove the element
list.splice(i--, 1);
// If we have some unicity property then
// we only need to do this once
if (flags.unique) {
break;
}
}
}
}
}
return this;
},
//...
fireWith: function (context, args) {
if (stack) {
if (firing) {
if (!flags.once) {
stack.push([context, args]);
}
} else if (!(flags.once && memory)) {
fire(context, args);
}
}
return this;
},
// ...
};
return self;
};
完全分析透jQuery.Callbacks的代码时,才真的明白:这一切为了
安全地访问一个数组!
jQuery.Callbacks使用闭包函数访问外部的公共变量——一个存储callback函数的数组,在具体操作这一个公共变量时,它要解决的问题简化如下(具体代码考虑的问题更多):
- 添加一个callback函数到数组中时,这个数组正在执行怎么办?
- 同样的,删除一个callback函数时,又怎么办?
- 触发数组执行的函数fire正在执行时,这个fire以新的参数再次被触发时,又怎么办呢?
实际上,这就是一个典型的线程安全问题。
在Java、C#这种后台语言中,可以直接用锁将这个公共变量锁起来,或者将使用这个公共变量的方法都加上synchronized关键字、代码加lock,或者还有更加不考虑性能的简单做法直接在类上加锁等等,这些后台语言处理这种并发问题的解决方案非常多。
但是对于弱类型前端脚本语言javascript,没有这样加锁阻塞外部线程访问的这样的关键字支持,就只能够通过在程序逻辑上更加严密。(忽然发觉,做了这么多项目的前端功能,我竟然从来没有考虑过多线程导致的正在循环的数组长度可能正在外部被改变的问题!不过毕竟一般的应用不太可能出问题,浏览器一个人操作,不可有并发的问题,除非你使用了很多setTimeout来另起线程的执行函数!)
但是对于jQuery这样一个想让大家都来使用的脚本框架,它的作者必须考虑得这么周全:只要是能够相像得到会出现问题的地方,就一定要解决,即使出问题的概率只有那么1%。这就是实现功能写的代码,与写框架代码的差别!
上述问题jQeury是这么解决的:
- 添加:先将正在执行循环长度加1,然后直接在数组后面添加callback函数。
- 删除:当前执行函数的索引减1,循环长度减1,直接删除callback函数。
- 使用一个stack数组,暂时存储新的fire调用时的参数。在当前循环执行结束时,检测stack中非空时,遍历stack来触发新一轮的callback数组循环调用。