翻开jQuery源码你会发现,ajax、Deferred、queue这些耳熟能详的模块,底层都依赖同一个工具:jQuery.Callbacks()。它负责管理一个回调函数列表,支持添加、删除、触发、锁定等操作。构造时传入的标志位字符串决定了这个列表的行为模式,比如$.Callbacks("once memory")就是Deferred实现的关键基础。下面从源码层面逐个拆解这些标志位的内部实现逻辑。

Callbacks的整体结构:options缓存与核心变量
Callbacks的核心是一个闭包,内部维护了几个关键变量:list保存回调函数数组,firingIndex记录当前触发位置,stack用于在触发过程中暂存新的fire请求,memory存储上次触发的上下文和参数。标志位在初始化时通过一个createOptions函数处理,把传入的字符串按空格切分,生成一个以标志名为属性、值为true的对象。
// 简化后的 createOptions 逻辑
function createOptions( options ) {
var object = {};
jQuery.each( options.match( rnothtmlwhite ) || [], function( _, flag ) {
object[ flag ] = true;
} );
return object;
}
// 使用方式
var options = createOptions( "once memory" );
// 得到 { once: true, memory: true }拿到options后,构造过程会预先确定一个重要行为:如果没有同时设置once,stack会是一个数组,用于触发过程中暂存新的fire调用;如果设置了once,stack直接为false,表示fire之后的重复触发会被丢弃。这个看似不起眼的预判,正是once语义实现的第一块拼图。
once标志:触发一次后永久锁定列表
once的实现比想象中更直接。看fire方法收尾部分的源码:
fire = function( data ) {
memory = options.memory && data;
fired = true;
firingIndex = firingStart || 0;
firingStart = 0;
firingLength = list.length;
for ( ; firingIndex < firingLength; firingIndex++ ) {
if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false
&& options.stopOnFalse ) {
break;
}
}
fired = false;
if ( list ) {
if ( stack ) {
if ( stack.length ) {
fire( stack.shift() );
}
} else if ( memory ) {
list = undefined;
} else {
self.disable();
}
}
};关键在最后一层判断。当once生效时stack为false,fire结束后不会走进stack分支。如果是once单独使用,走self.disable()分支,列表被清空且彻底禁用,之后调用add会静默失败(源码中对list存在性的检查会直接跳过)。而once memory组合走的是list = undefined分支,注意这里并没有禁用整个self,为memory的补发机制留下了通道,这是两个组合的本质区别。
还要注意一点:once的锁定动作发生在触发流程的收尾阶段,而不是在fire开始前。这保证了当前这一轮循环中的所有回调都能完整执行完毕,锁定只影响后续的行为。
memory标志:触发后add立即补发的记忆机制
memory是最精妙的标志位。它的作用是:列表一旦被fire过,之后再add进来的函数会立即用上次触发的参数被调用。Deferred的done、fail回调能在异步完成后注册并依然拿到结果,靠的就是这套机制。
源码中memory的写入发生在fire的开头:memory = options.memory && data。注意这里用的是短路求值,如果没开memory,变量永远保持undefined;如果开了,memory保存的是完整的触发数据(上下文加参数数组)。而add方法的末尾有一段关键的补发逻辑:
add = function() {
if ( list ) {
var start = list.length;
addToList( arguments );
if ( firing ) {
// 正在触发中,扩展本轮循环的长度
firingLength = list.length;
} else if ( memory ) {
// 有记忆,从新添加的位置开始补发
firingStart = start;
fire( memory );
}
}
return this;
};这段代码揭示了两个要点。第一,触发过程中add的函数会被追加到列表尾部,并通过更新firingLength让当前正在进行的循环也能执行到它们,这就是Callbacks支持动态添加的原理。第二,非触发状态下如果memory有值,就从新添加的位置开始fire,用记忆的数据补发,老回调不会重复执行,这种设计非常克制。
再回头看once memory的组合:fire收尾时执行list = undefined,之后add虽然不会再push函数,但add入口处还有一个判断——如果memory存在,即使list为undefined,新函数也会被直接用记忆数据调用一次。这正是Promise的状态固化语义:状态确定后,后续注册的回调依然能拿到结果,但永远不会被再次触发。
unique与stopOnFalse:去重判断与中断联动
unique的作用是阻止同一个函数被重复添加,判断发生在add阶段,内部通过inArray做线性查找。这里有一个常见误区必须指出:去重是引用级别的比对,两个逻辑完全相同的匿名函数会被视为不同对象,去重完全无效。想让unique生效,必须持有函数的同一个引用。
var cb = $.Callbacks( "unique" );
function handler() { console.log( "run" ); }
cb.add( handler );
cb.add( handler ); // 被去重,列表中只有一个handler
cb.add( function() {} ); // 匿名函数是新引用,不会被去重
cb.fire(); // 输出一次 runstopOnFalse的逻辑就藏在fire的循环里,也就是前文代码中的=== false && options.stopOnFalse判断。任何一个回调返回false,循环立即break,后续回调不再执行。但很多人忽略了一点:如果不加once,stack中排队的后续fire请求仍会继续执行,中断的只是当前这一轮的回调链,列表本身并未被禁用。
用一个表格总结四种标志位的行为差异:
| 标志位 | 生效阶段 | 核心行为 |
|---|---|---|
| once | fire收尾 | 触发一次后锁定列表,后续add静默失效 |
| memory | fire开头记录,add末尾补发 | 新回调用上次参数立即调用一次 |
| unique | add阶段 | 按函数引用去重,匿名函数无效 |
| stopOnFalse | fire循环中 | 回调返回false即中断本轮触发 |
把这四个标志位串起来看,Callbacks的设计哲学是用最小的状态变量组合出丰富的行为:list、stack、memory三个变量配合options里的布尔开关,就支撑起了Promise的完整语义。理解了这一点,再去读Deferred的源码,你会发现resolve、reject不过是Callbacks带不同标志位的封装,原本复杂的异步流程瞬间清晰了。
jQuery Callbacks标志位回调列表修改时间:2026-09-14 09:07:10