[toc]
## Callbacks 的使用
jQuery 內部提供了很多基礎功能的方法,比如 $.ajax()、$.each() 和 $.Callbacks(),這些方法既可以在內部進行使用,又可以被開發者拿到外部單獨使用。
Callbacks 的支持的方法有幾個主要的,add、fire、remove 和 disable,比如官方有一個例子:
```
// 這兩個作為 callback 函數
function fn1( value ) {
console.log( value );
}
function fn2( value ) {
fn1("fn2 says: " + value);
return false;
}
// 調用 jQuery 的 Callbacks 生成 callbacks
var callbacks = $.Callbacks();
callbacks.add( fn1 );
callbacks.fire( "foo!" );
// 'foo!'
callbacks.add( fn2 );
callbacks.fire( "bar!" );
// 'bar!'
// 'fn2 says: bar!'
```
從基本 demo 可以看出,$.Callbacks() 函數生成了一個 callbacks 對象,這個對象的 .add() 方法是添加回調函數,而 .fire() 方法則是執行回調函數。
.remove() 方法是移除回調函數:
```
var callbacks = $.Callbacks();
callbacks.add( fn1 );
callbacks.fire( "foo!" );
// 'foo!'
callbacks.add( fn2 );
callbacks.fire( "bar!" );
// 'bar!'
// 'fn2 says: bar!'
callbacks.remove( fn2 );
callbacks.fire( "foobar" );
// 'foobar'
```
$.Callbacks() 還支持幾個參數,表示執行回調的幾種效果,$.Callbacks('once'):
- once: 確保這個回調列表只執行 .fire() 一次(像一個遞延 Deferred)
- memory: 保持以前的值,將添加到這個列表的后面的最新的值立即執行調用任何回調 (像一個遞延 Deferred)
- unique: 確保一次只能添加一個回調(所以在列表中沒有重復的回調)
- stopOnFalse: 當一個回調返回false 時中斷調用
此方法還支持多個參數,比如$.Callbacks('once memory'),具體的使用請參考這個鏈接。
## Callbacks 的源碼
在放 jQuery 3.0 的源碼之前,我們先來簡單的模擬一下 Callbacks 函數,來實現其基本的功能:
```
var Callbacks = function(){
var Cb = {
callbacks: [],
add: function(fn){
this.callbacks.push(fn);
return this;
},
fire: function(value){
this.callbacks.forEach(function(fn){
fn(value);
});
return this;
}
}
return Cb;
}
// 測試
var callbacks = Callbacks();
callbacks.add(fn1);
callbacks.fire('test'); //'test'
```
可以看到其實一個簡單的 Callbacks 函數實現起來還是非常簡單的。
整個的 Callbacks 源碼其實大致如下:
```
jQuery.Callbacks = function(options){
// 先對參數進行處理,比如 once、unique 等
options = createOptions(options);
// 參數定義,包括一些 flag 和 callbacks 數組
var list = [], queue = [] ...
// fire 是遍歷數組,回掉函數的執行
var fire = function(){
...
}
// self 是最終返回的對象
var self = {
add: function(){...},
remove: function(){...},
has: function(){...},
disable: function(){...},
fireWith: function(){...},//這個其實是 fire 函數的執行
fire: function(){...}
...
}
return self;
}
```
因為前面已經簡單的介紹過了如何實現一個基本的 Callbacks 函數,這里稍微清晰了一點,來看下 createOptions 函數,這個函數主要是對類似于 $.Callbacks('once memory')類型對 callback 進行 flag 分離:
```
function createOptions(options) {
var object = {};
jQuery.each(options.match(rnothtmlwhite) || [], function (_, flag) {
object[flag] = true;
});
return object;
}
```
其中 rnothtmlwhite 是一個正則表達式` /[^\x20\t\r\n\f]+/g`,用來獲得所有的 flag 標志。createOptions 的結果是一個對象,鍵值分別是 flag 和 boolean。
那么現在的主要的問題,就全在那些 flag 上面來,"once memory unique stopOnFalse"。
源碼奉上:
```
jQuery.Callbacks = function(options) {
// flag 處理
options = typeof options === "string" ? createOptions(options) : jQuery.extend({}, options);
var // Flag to know if list is currently firing
firing,
// Last fire value for non-forgettable lists
memory,
// Flag to know if list was already fired
fired,
// Flag to prevent firing
locked,
// Actual callback list
list = [],
// Queue of execution data for repeatable lists
queue = [],
// Index of currently firing callback (modified by add/remove as needed)
firingIndex = -1,
// Fire callbacks
fire = function() {
// 只執行一次,以后都不執行了
locked = locked || options.once;
// Execute callbacks for all pending executions,
// respecting firingIndex overrides and runtime changes
fired = firing = true;
for (; queue.length; firingIndex = -1) {
memory = queue.shift();
while (++firingIndex < list.length) {
// 回調執行函數,并檢查是否 stopOnFalse,并阻止繼續運行
if (list[firingIndex].apply(memory[0], memory[1]) === false && options.stopOnFalse) {
// Jump to end and forget the data so .add doesn't re-fire
firingIndex = list.length;
memory = false;
}
}
}
// Forget the data if we're done with it
if (!options.memory) {
memory = false;
}
firing = false;
// locked 在這里實現
if (locked) {
// 雖然鎖住但是是 memory,保留 list 以后使用
if (memory) {
list = [];
// 拜拜...
} else {
list = "";
}
}
},
// Actual Callbacks object
self = {
// Add a callback or a collection of callbacks to the list
add: function() {
if (list) {
// If we have memory from a past run, we should fire after adding
if (memory && !firing) {
firingIndex = list.length - 1;
queue.push(memory);
}
(function add(args) {
jQuery.each(args, function(_, arg) {
if (jQuery.isFunction(arg)) {
if (!options.unique || !self.has(arg)) {
list.push(arg);
}
} else if (arg && arg.length && jQuery.type(arg) !== "string") {
// Inspect recursively
add(arg);
}
});
})(arguments);
if (memory && !firing) {
fire();
}
}
return this;
},
// Remove a callback from the list
remove: function() {
jQuery.each(arguments, function(_, arg) {
var index;
while ((index = jQuery.inArray(arg, list, index)) > -1) {
list.splice(index, 1);
// Handle firing indexes
if (index <= firingIndex) {
firingIndex--;
}
}
});
return this;
},
// Check if a given callback is in the list.
// If no argument is given, return whether or not list has callbacks attached.
has: function(fn) {
return fn ? jQuery.inArray(fn, list) > -1 : list.length > 0;
},
// Remove all callbacks from the list
empty: function() {
if (list) {
list = [];
}
return this;
},
// Disable .fire and .add
// Abort any current/pending executions
// Clear all callbacks and values
disable: function() {
locked = queue = [];
list = memory = "";
return this;
},
disabled: function() {
return !list;
},
// Disable .fire
// Also disable .add unless we have memory (since it would have no effect)
// Abort any pending executions
lock: function() {
locked = queue = [];
if (!memory && !firing) {
list = memory = "";
}
return this;
},
locked: function() {
return !!locked;
},
// Call all callbacks with the given context and arguments
fireWith: function(context, args) {
if (!locked) {
args = args || [];
args = [context, args.slice ? args.slice() : args];
queue.push(args);
if (!firing) {
fire();
}
}
return this;
},
// Call all the callbacks with the given arguments
fire: function() {
self.fireWith(this, arguments);
return this;
},
// To know if the callbacks have already been called at least once
fired: function() {
return !!fired;
}
};
return self;
};
```
總的來說,這種 pub/sub 模式的代碼還是比較容易看懂的,有些疑問的地方,比如源碼中其實有兩個數組,list 是隊列數組,本應該叫做 queue,但是 queue 數組已經被定義,且 queue 的作用是用來存儲 fire 執行時的參數,這點不能搞混。
還有就是當整個代碼 firing 這個參數,導致當函數正在運行的時候,即執行兩次 fire 的時候,需要補充 queue 元素,但 fire() 函數只執行一次。
## 總結
jQuery.Callbacks 沿用 jQuery 一貫的套路,最后 return self,剛看第一遍第二遍的時候,有點模模糊糊的,主要還是 once、memory 等 flag 參數干擾我的視線,尤其是其這些 flag 標志的實現,難受。