Callbacks 模塊並不是必備的模塊,其作用是管理回調函數,為 Defferred 模塊提供支持,Defferred 模塊又為 Ajax 模塊的 風格提供支持,接下來很快就會分析到 Ajax模塊,在此之前,先看 Callbacks 模塊和 Defferred 模塊的實現。 源碼版本 本文閱讀的源 ...
Callbacks 模塊並不是必備的模塊,其作用是管理回調函數,為 Defferred 模塊提供支持,Defferred 模塊又為 Ajax 模塊的 promise
風格提供支持,接下來很快就會分析到 Ajax模塊,在此之前,先看 Callbacks 模塊和 Defferred 模塊的實現。
源碼版本
本文閱讀的源碼為 zepto1.2.0
整體結構
將 Callbacks 模塊的代碼精簡後,得到的結構如下:
;(function($){
$.Callbacks = function(options) {
...
Callbacks = {
...
}
return Callbacks
}
})(Zepto)
其實就是向 zepto
對象上,添加了一個 Callbacks
函數,這個是一個工廠函數,調用這個函數返回的是一個對象,對象內部包含了一系列的方法。
options
參數為一個對象,在源碼的內部,作者已經註釋了各個鍵值的含義。
// Option flags:
// - once: Callbacks fired at most one time.
// - memory: Remember the most recent context and arguments
// - stopOnFalse: Cease iterating over callback list
// - unique: Permit adding at most one instance of the same callback
once: 回調至多只能觸發一次
memory: 記下最近一次觸發的上下文及參數列表,再添加新回調的時候都立刻用這個上下文及參數立即執行
stopOnFalse: 如果隊列中有回調返回 `false`,立即中止後續回調的執行
unique: 同一個回調只能添加一次
全局變數
options = $.extend({}, options)
var memory, // Last fire value (for non-forgettable lists)
fired, // Flag to know if list was already fired
firing, // Flag to know if list is currently firing
firingStart, // First callback to fire (used internally by add and fireWith)
firingLength, // End of the loop when firing
firingIndex, // Index of currently firing callback (modified by remove if needed)
list = [], // Actual callback list
stack = !options.once && [], // Stack of fire calls for repeatable lists
options
: 構造函數的配置,預設為空對象list
: 回調函數列表stack
: 列表可以重覆觸發時,用來緩存觸發過程中未執行的任務參數,如果列表只能觸發一次,stack
永遠為false
memory
: 記憶模式下,會記住上一次觸發的上下文及參數fired
: 回調函數列表已經觸發過firing
: 回調函數列表正在觸發firingStart
: 回調任務的開始位置firingIndex
: 當前回調任務的索引firingLength
:回調任務的長度
基礎用法
我用 jQuery
和 Zepto
的時間比較短,之前也沒有直接用過 Callbacks
模塊,單純看代碼不易理解它是怎樣工作的,在分析之前,先看一下簡單的 API
調用,可能會有助於理解。
var callbacks = $.Callbacks({memory: true})
var a = function(a) {
console.log('a ' + a)
}
var b = function(b) {
console.log('b ' + b)
}
var c = function(c) {
console.log('c ' + c)
}
callbacks.add(a).add(b).add(c) // 向隊列 list 中添加了三個回調
callbacks.remove(c) // 刪除 c
callbacks.fire('fire')
// 到這步輸出了 `a fire` `b fire` 沒有輸出 `c fire`
callbacks.lock()
callbacks.fire('fire after lock') // 到這步沒有任何輸出
// 繼續向隊列添加回調,註意 `Callbacks` 的參數為 `memory: true`
callbacks.add(function(d) {
console.log('after lock')
})
// 輸出 `after lock`
callbacks.disable()
callbacks.add(function(e) {
console.log('after disable')
})
// 沒有任何輸出
上面的例子只是簡單的調用,也有了註釋,下麵開始分析 API
內部方法
fire
fire = function(data) {
memory = options.memory && data
fired = true
firingIndex = firingStart || 0
firingStart = 0
firingLength = list.length
firing = true
for ( ; list && firingIndex < firingLength ; ++firingIndex ) {
if (list[firingIndex].apply(data[0], data[1]) === false && options.stopOnFalse) {
memory = false
break
}
}
firing = false
if (list) {
if (stack) stack.length && fire(stack.shift())
else if (memory) list.length = 0
else Callbacks.disable()
}
}
Callbacks
模塊只有一個內部方法 fire
,用來觸發 list
中的回調執行,這個方法是 Callbacks
模塊的核心。
變數初始化
memory = options.memory && data
fired = true
firingIndex = firingStart || 0
firingStart = 0
firingLength = list.length
firing = true
fire
只接收一個參數 data
,這個內部方法 fire
跟我們調用 API
所接收的參數不太一樣,這個 data
是一個數組,數組裡面只有兩項,第一項是上下文對象,第二項是回調函數的參數數組。
如果 options.memory
為 true
,則將 data
,也即上下文對象和參數保存下來。
將 list
是否已經觸發過的狀態 fired
設置為 true
。
將當前回調任務的索引值 firingIndex
指向回調任務的開始位置 firingStart
或者回調列表的開始位置。
將回調列表的開始位置 firingStart
設置為回調列表的開始位置。
將回調任務的長度 firingLength
設置為回調列表的長度。
將回調的開始狀態 firing
設置為 true
執行回調
for ( ; list && firingIndex < firingLength ; ++firingIndex ) {
if (list[firingIndex].apply(data[0], data[1]) === false && options.stopOnFalse) {
memory = false
break
}
}
firing = false
執行回調的整體邏輯是遍歷回調列表,逐個執行回調。
迴圈的條件是,列表存在,並且當前回調任務的索引值 firingIndex
要比回調任務的長度要小,這個很容易理解,當前的索引值都超出了任務的長度,就找不到任務執行了。
list[firingIndex].apply(data[0], data[1])
就是從回調列表中找到對應的任務,綁定上下文對象,和傳入對應的參數,執行任務。
如果回調執行後顯式返回 false
, 並且 options.stopOnFalse
設置為 true
,則中止後續任務的執行,並且清空 memory
的緩存。
回調任務執行完畢後,將 firing
設置為 false
,表示當前沒有正在執行的任務。
檢測未執行的回調及清理工作
if (list) {
if (stack) stack.length && fire(stack.shift())
else if (memory) list.length = 0
else Callbacks.disable()
}
列表任務執行完畢後,先檢查 stack
中是否有沒有執行的任務,如果有,則將任務參數取出,調用 fire
函數執行。後面會看到,stack
儲存的任務是 push
進去的,用 shift
取出,表明任務執行的順序是先進先出。
memory
存在,則清空回調列表,用 list.length = 0
是清空列表的一個方法。在全局參數中,可以看到, stack
為 false
,只有一種情況,就是 options.once
為 true
的時候,表示任務只能執行一次,所以要將列表清空。而 memory
為 true
,表示後面添加的任務還可以執行,所以還必須保持 list
容器的存在,以便後續任務的添加和執行。
其他情況直接調用 Callbacks.disable()
方法,禁用所有回調任務的添加和執行。
.add()
add: function() {
if (list) {
var start = list.length,
add = function(args) {
$.each(args, function(_, arg){
if (typeof arg === "function") {
if (!options.unique || !Callbacks.has(arg)) list.push(arg)
}
else if (arg && arg.length && typeof arg !== 'string') add(arg)
})
}
add(arguments)
if (firing) firingLength = list.length
else if (memory) {
firingStart = start
fire(memory)
}
}
return this
},
start
為原來回調列表的長度。保存起來,是為了後面修正回調任務的開始位置時用。
內部方法add
add = function(args) {
$.each(args, function(_, arg){
if (typeof arg === "function") {
if (!options.unique || !Callbacks.has(arg)) list.push(arg)
}
else if (arg && arg.length && typeof arg !== 'string') add(arg)
})
}
add
方法的作用是將回調函數 push
進回調列表中。參數 arguments
為數組或者偽數組。
用 $.each
方法來遍歷 args
,得到數組項 arg
,如果 arg
為 function
類型,則進行下一個判斷。
在下一個判斷中,如果 options.unique
不為 true
,即允許重覆的回調函數,或者原來的列表中不存在該回調函數,則將回調函數存入回調列表中。
如果 arg
為數組或偽數組(通過 arg.length
是否存在判斷,並且排除掉 string
的情況),再次調用 add
函數分解。
修正回調任務控制變數
add(arguments)
if (firing) firingLength = list.length
else if (memory) {
firingStart = start
fire(memory)
}
調用 add
方法,向列表中添加回調函數。
如果回調任務正在執行中,則修正回調任務的長度 firingLength
為當前任務列表的長度,以便後續添加的回調函數可以執行。
否則,如果為 memory
模式,則將執行回調任務的開始位置設置為 start
,即原來列表的最後一位的下一位,也就是新添加進列表的第一位,然後調用 fire
,以緩存的上下文及參數 memory
作為 fire
的參數,立即執行新添加的回調函數。
.remove()
remove: function() {
if (list) {
$.each(arguments, function(_, arg){
var index
while ((index = $.inArray(arg, list, index)) > -1) {
list.splice(index, 1)
// Handle firing indexes
if (firing) {
if (index <= firingLength) --firingLength
if (index <= firingIndex) --firingIndex
}
}
})
}
return this
},
刪除列表中指定的回調。
刪除回調函數
用 each
遍歷參數列表,在 each
遍歷里再有一層 while
迴圈,迴圈的終止條件如下:
(index = $.inArray(arg, list, index)) > -1
$.inArray()
最終返回的是數組項在數組中的索引值,如果不在數組中,則返回 -1
,所以這個判斷是確定回調函數存在於列表中。關於 $.inArray
的分析,見《讀zepto源碼之工具函數》。
然後調用 splice
刪除 list
中對應索引值的數組項,用 while
迴圈是確保列表中有重覆的回調函數都會被刪除掉。
修正回調任務控制變數
if (firing) {
if (index <= firingLength) --firingLength
if (index <= firingIndex) --firingIndex
}
如果回調任務正在執行中,因為回調列表的長度已經有了變化,需要修正回調任務的控制參數。
如果 index <= firingLength
,即回調函數在當前的回調任務中,將回調任務數減少 1
。
如果 index <= firingIndex
,即在正在執行的回調函數前,將正在執行函數的索引值減少 1
。
這樣做是防止回調函數執行到最後時,沒有找到對應的任務執行。
.fireWith
fireWith: function(context, args) {
if (list && (!fired || stack)) {
args = args || []
args = [context, args.slice ? args.slice() : args]
if (firing) stack.push(args)
else fire(args)
}
return this
},
以指定回調函數的上下文的方式來觸發回調函數。
fireWith
接收兩個參數,第一個參數 context
為上下文對象,第二個 args
為參數列表。
fireWith
後續執行的條件是列表存在並且回調列表沒有執行過或者 stack
存在(可為空數組),這個要註意,後面講 disable
方法和 lock
方法區別的時候,這是一個很重要的判斷條件。
args = args || []
args = [context, args.slice ? args.slice() : args]
先將 args
不存在時,初始化為數組。
再重新組合成新的變數 args
,這個變數的第一項為上下文對象 context
,第二項為參數列表,調用 args.slice
是對數組進行拷貝,因為 memory
會儲存上一次執行的上下文對象及參數,應該是怕外部對引用的更改的影響。
if (firing) stack.push(args)
else fire(args)
如果回調正處在觸發的狀態,則將上下文對象和參數先儲存在 stack
中,從內部函數 fire
的分析中可以得知,回調函數執行完畢後,會從 stack
中將 args
取出,再觸發 fire
。
否則,觸發 fire
,執行回調函數列表中的回調函數。
add
和 remove
都要判斷 firing
的狀態,來修正回調任務控制變數,fire
方法也要判斷 firing
,來判斷是否需要將 args
存入 stack
中,但是 javascript
是單線程的,照理應該不會出現在觸發的同時 add
或者 remove
或者再調用 fire
的情況。
.fire()
fire: function() {
return Callbacks.fireWith(this, arguments)
},
fire
方法,用得最多,但是卻非常簡單,調用的是 fireWidth
方法,上下文對象是 this
。
.has()
has: function(fn) {
return !!(list && (fn ? $.inArray(fn, list) > -1 : list.length))
},
has
有兩個作用,如果有傳參時,用來查測所傳入的 fn
是否存在於回調列表中,如果沒有傳參時,用來檢測回調列表中是否已經有了回調函數。
fn ? $.inArray(fn, list) > -1 : list.length
這個三元表達式前面的是判斷指定的 fn
是否存在於回調函數列表中,後面的,如果 list.length
大於 0
,則回調列表已經存入了回調函數。
.empty()
empty: function() {
firingLength = list.length = 0
return this
},
empty
的作用是清空回調函數列表和正在執行的任務,但是 list
還存在,還可以向 list
中繼續添加回調函數。
.disable()
disable: function() {
list = stack = memory = undefined
return this
},
disable
是禁用回調函數,實質是將回調函數列表置為 undefined
,同時也將 stack
和 memory
置為 undefined
,調用 disable
後,add
、remove
、fire
、fireWith
等方法不再生效,這些方法的首要條件是 list
存在。
.disabled()
disabled: function() {
return !list
},
回調是否已經被禁止,其實就是檢測 list
是否存在。
.lock()
lock: function() {
stack = undefined
if (!memory) Callbacks.disable()
return this
},
鎖定回調列表,其實是禁止 fire
和 fireWith
的執行。
其實是將 stack
設置為 undefined
, memory
不存在時,調用的是 disable
方法,將整個列表清空。效果等同於禁用回調函數。fire
和 add
方法都不能再執行。
.lock() 和 .disable() 的區別
為什麼 memory
存在時,stack
為 undefined
就可以將列表的 fire
和 fireWith
禁用掉呢?在上文的 fireWith
中,我特別提到了 !fired || stack
這個判斷條件。在 stack
為 undefined
時,fireWith
的執行條件看 fired
這個條件。如果回調列表已經執行過, fired
為 true
,fireWith
不會再執行。如果回調列表沒有執行過,memory
為 undefined
,會調用 disable
方法禁用列表,fireWith
也不能執行。
所以,disable
和 lock
的區別主要是在 memory
模式下,回調函數觸發過後,lock
還可以調用 add
方法,向回調列表中添加回調函數,添加完畢後會立刻用 memory
的上下文和參數觸發回調函數。
.locked()
locked: function() {
return !stack
},
回調列表是否被鎖定。
其實就是檢測 stack
是否存在。
.fired()
fired: function() {
return !!fired
}
回調列表是否已經被觸發過。
回調列表觸發一次後 fired
就會變為 true
,用 !!
的目的是將 undefined
轉換為 false
返回。
系列文章
- 讀Zepto源碼之代碼結構
- 讀 Zepto 源碼之內部方法
- 讀Zepto源碼之工具函數
- 讀Zepto源碼之神奇的$
- 讀Zepto源碼之集合操作
- 讀Zepto源碼之集合元素查找
- 讀Zepto源碼之操作DOM
- 讀Zepto源碼之樣式操作
- 讀Zepto源碼之屬性操作
- 讀Zepto源碼之Event模塊
- 讀Zepto源碼之IE模塊
參考
License
作者:對角另一面