jQuery 的 $.Callbacks() 是一个功能强大的回调函数管理器,它允许你创建回调列表,然后通过 fire() 依次触发列表中的函数。这个机制在 jQuery 内部被广泛应用于 Deferred 对象、动画队列、Ajax 回调等场景。在实际开发中,有时会遇到这样的需求:当满足某个特定条件后,希望回调列表彻底失效,之后无论怎么添加新函数或者调用 fire,都不再执行任何回调。这时就可以使用 disabled() 方法。

理解 $.Callbacks() 与 disabled() 的工作机制
$.Callbacks() 可以接受一个可选的 flags 字符串参数,用来控制回调列表的行为,比如 "once" 保证列表只被触发一次,"memory" 会记住上一次触发时的参数并立即调用后续添加的回调,"unique" 避免重复添加同一个函数。而 disabled() 方法则比较特殊,它会直接禁用整个回调列表。一旦调用,列表就进入永久禁用状态,无法再通过任何方式恢复。
从源码层面看,jQuery 的 Callbacks 对象内部维护了一个 disabled 标记。当调用 disabled() 时,该标记被设为 true,同时内部的函数列表会被清空。此后任何对 add() 的调用都会被忽略,任何对 fire() 或 fireWith() 的调用也直接返回,不会触发任何函数。这种设计保证了禁用操作是原子性的,不会出现半禁用状态。
下面先看一个最基础的例子:创建回调列表,添加两个回调函数,然后调用 disabled(),再尝试 fire。可以看到控制台只输出了前两次触发的结果,禁用后的 fire 没有任何输出。
// 创建一个普通的 Callbacks 列表
var callbacks = $.Callbacks();
// 添加两个回调函数
callbacks.add(function(msg) {
console.log("第一个回调: " + msg);
});
callbacks.add(function(msg) {
console.log("第二个回调: " + msg);
});
// 正常触发一次
callbacks.fire("正常触发");
// 输出:
// 第一个回调: 正常触发
// 第二个回调: 正常触发
// 禁用回调列表
callbacks.disabled();
// 再次触发,不会有任何输出
callbacks.fire("禁用后触发");
// 尝试添加新回调,也不会生效
callbacks.add(function(msg) {
console.log("新添加的回调: " + msg);
});
callbacks.fire("禁用后添加再触发");
// 控制台没有任何额外输出
在特定条件下触发 disabled() 的典型场景
实际业务中,很少会无条件地禁用回调列表,通常是在某个状态满足时决定“到此为止”。比较经典的场景是错误处理:当异步操作第一次失败时,立即禁用回调列表,防止后续成功或失败的回调继续干扰 UI 状态。例如一个带有重试机制的请求,我们只关心第一次失败时给用户展示错误提示,后续重试的结果不再触发任何逻辑。
下面的代码模拟了一个网络请求状态机。当请求失败次数超过 3 次时,就调用 disabled() 永久关闭回调列表。这样即使后面又成功返回了数据,也不会再执行任何回调函数。
// 模拟请求管理器
var requestCallbacks = $.Callbacks("memory");
var failCount = 0;
// 注册成功和失败处理
requestCallbacks.add(function(result) {
console.log("请求成功,数据为:" + result);
});
requestCallbacks.add(function(error) {
console.log("请求失败,错误信息:" + error);
});
// 模拟请求函数
function simulateRequest(success, data) {
if (success) {
requestCallbacks.fire("success", data);
} else {
failCount++;
if (failCount > 3) {
// 失败次数超过 3 次,永久禁用回调列表
requestCallbacks.disabled();
console.log("失败次数过多,回调列表已禁用");
} else {
requestCallbacks.fire("fail", "网络错误");
}
}
}
// 触发三次失败
simulateRequest(false, null);
simulateRequest(false, null);
simulateRequest(false, null);
// 输出:
// 请求失败,错误信息:网络错误
// 请求失败,错误信息:网络错误
// 请求失败,错误信息:网络错误
// 第四次失败,触发禁用
simulateRequest(false, null);
// 输出:
// 失败次数过多,回调列表已禁用
// 即使后面成功,也不会再触发任何回调
simulateRequest(true, "有效数据");
// 无任何输出
另一个常见场景是“一次性事件”的变体。有些回调列表希望只在某个条件第一次满足时执行一次,之后无论条件是否再次满足都不再响应。直接用 "once" flag 也能实现,但它只限制 fire 次数,无法阻止添加新回调。而 disabled() 可以从根源上关闭整个机制,语义更明确。
disabled()、lock() 与 empty() 的区别
很多开发者容易把 disabled() 和 lock() 混为一谈,它们虽然都影响回调的触发,但行为有本质区别。lock() 会锁定列表,使得后续的 fire() 调用被忽略,但已经添加的回调函数仍然保留在列表中。如果之后调用 add() 添加新函数,并且列表带有 "memory" flag,新函数会立即用上次 fire 的参数调用。而 disabled() 不仅阻止 fire,还会清空函数列表并阻止任何新的 add 操作。
另一个相关方法是 empty(),它只清空当前列表中的函数,但列表本身仍然可用,之后可以继续添加新函数并触发。与之相比,disabled() 是彻底的“终结”,没有回旋余地。
下面的对比表格展示了三者的差异:
| 方法 | 是否保留已添加回调 | 是否允许后续 add | 是否允许 fire | 能否恢复 |
|---|---|---|---|---|
empty() | 否(清空) | 是 | 是 | 是(重新添加即可) |
lock() | 是 | 是(memory 时立即触发) | 否(后续 fire 被忽略) | 否(但列表仍可添加函数) |
disabled() | 否(清空) | 否(忽略所有 add) | 否 | 否(永久禁用) |
理解这些差异有助于在正确的场景选用正确的方法。比如需要暂停触发但保留函数,应该用 lock();需要清空但可能重用列表,用 empty();需要一次性关闭整个生命周期,用 disabled()。
实现条件禁用时的工程实践与注意事项
虽然 disabled() 用起来很简单,但在复杂项目中滥用可能导致难以追踪的状态问题。因为一旦禁用,列表就永远死了,后续代码如果还持有这个列表的引用并继续添加回调,那些回调会静默丢失,不会报错。这可能会掩盖逻辑错误。因此建议在调用 disabled() 之前,确保所有可能的添加方都已经完成,或者使用文档注释明确该列表的生命周期。
另一种更可控的做法是使用状态标记自己管理“禁用”逻辑,配合 lock() 或条件判断,以便未来有可能恢复。例如维护一个 isDisabled 布尔值,在 fire() 前检查它。但这样就需要额外封装,而 $.Callbacks() 本身已经提供了内置的禁用机制,直接使用 disabled() 更符合 jQuery 的语义。
此外,需要注意 disabled() 在带 "memory" flag 的列表中的行为。如果列表是 "memory" 类型,并且之前已经 fire 过,那么调用 disabled() 后,后续添加的回调不会收到上次的参数,因为列表已经完全关闭了。这与 lock() 的行为形成对比。下面的代码演示了这种差异:
// 带 memory 的列表
var memCallbacks = $.Callbacks("memory");
memCallbacks.add(function(val) {
console.log("memory 回调: " + val);
});
// 触发一次,参数被记住
memCallbacks.fire("第一次参数");
// 输出:memory 回调: 第一次参数
// 先 lock,再添加新回调,会立即用上次参数触发
memCallbacks.lock();
memCallbacks.add(function(val) {
console.log("lock 后添加的回调: " + val);
});
// 输出:lock 后添加的回调: 第一次参数
// 再创建一个 memory 列表,用 disabled 对比
var memCallbacks2 = $.Callbacks("memory");
memCallbacks2.add(function(val) {
console.log("memory2 回调: " + val);
});
memCallbacks2.fire("第二次参数");
// 输出:memory2 回调: 第二次参数
memCallbacks2.disabled();
memCallbacks2.add(function(val) {
console.log("disabled 后添加的回调: " + val);
});
// 没有任何输出,因为 disabled 后 add 被忽略
通过上面的示例可以清楚地看到,disabled() 比 lock() 更加彻底。在实际编码时,应该根据业务对“永久”的定义来决策。如果确定某个回调列表在一个条件满足后绝不会再使用,那么 disabled() 是最好的选择,它能够避免内存泄漏和逻辑混乱。
最后提醒一点,$.Callbacks() 属于 jQuery 底层工具,现代前端开发中可能更倾向使用 Promise、EventEmitter 等原生或社区方案。但在维护老旧 jQuery 项目或理解 jQuery 内部机制时,掌握 disabled() 的用法仍然很有价值。理解它的永久性、不可逆性以及与 lock() 的差异,能帮助你写出更健壮的代码。

