讀Zepto源碼之Callbacks模塊

Callbacks 模塊並非必備的模塊,其做用是管理回調函數,爲 Defferred 模塊提供支持,Defferred 模塊又爲 Ajax 模塊的 promise 風格提供支持,接下來很快就會分析到 Ajax模塊,在此以前,先看 Callbacks 模塊和 Defferred 模塊的實現。javascript

讀 Zepto 源碼系列文章已經放到了github上,歡迎star: reading-zeptohtml

源碼版本

本文閱讀的源碼爲 zepto1.2.0java

總體結構

將 Callbacks 模塊的代碼精簡後,獲得的結構以下:git

;(function($){
  $.Callbacks = function(options) {
    ...
    Callbacks = {
      ...
    }
    return Callbacks
  }
})(Zepto)複製代碼

其實就是向 zepto 對象上,添加了一個 Callbacks 函數,這個是一個工廠函數,調用這個函數返回的是一個對象,對象內部包含了一系列的方法。github

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:回調任務的長度

基礎用法

我用 jQueryZepto 的時間比較短,以前也沒有直接用過 Callbacks 模塊,單純看代碼不易理解它是怎樣工做的,在分析以前,先看一下簡單的 API 調用,可能會有助於理解。promise

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 是一個數組,數組裏面只有兩項,第一項是上下文對象,第二項是回調函數的參數數組。app

若是 options.memorytrue ,則將 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 是清空列表的一個方法。在全局參數中,能夠看到, stackfalse ,只有一種狀況,就是 options.oncetrue 的時候,表示任務只能執行一次,因此要將列表清空。而 memorytrue ,表示後面添加的任務還能夠執行,因此還必須保持 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,若是 argfunction 類型,則進行下一個判斷。

在下一個判斷中,若是 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,執行回調函數列表中的回調函數。

addremove 都要判斷 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 ,同時也將 stackmemory 置爲 undefined ,調用 disable 後,addremovefirefireWith 等方法再也不生效,這些方法的首要條件是 list 存在。

.disabled()

disabled: function() {
  return !list
},複製代碼

回調是否已經被禁止,其實就是檢測 list 是否存在。

.lock()

lock: function() {
  stack = undefined
  if (!memory) Callbacks.disable()
  return this
},複製代碼

鎖定回調列表,實際上是禁止 firefireWith 的執行。

實際上是將 stack 設置爲 undefinedmemory 不存在時,調用的是 disable 方法,將整個列表清空。效果等同於禁用回調函數。fireadd 方法都不能再執行。

.lock() 和 .disable() 的區別

爲何 memory 存在時,stackundefined 就能夠將列表的 firefireWith 禁用掉呢?在上文的 fireWith 中,我特別提到了 !fired || stack 這個判斷條件。在 stackundefined 時,fireWith 的執行條件看 fired 這個條件。若是回調列表已經執行過, firedtruefireWith 不會再執行。若是回調列表沒有執行過,memoryundefined ,會調用 disable 方法禁用列表,fireWith 也不能執行。

因此,disablelock 的區別主要是在 memory 模式下,回調函數觸發事後,lock 還能夠調用 add 方法,向回調列表中添加回調函數,添加完畢後會馬上用 memory 的上下文和參數觸發回調函數。

.locked()

locked: function() {
  return !stack
},複製代碼

回調列表是否被鎖定。

其實就是檢測 stack 是否存在。

.fired()

fired: function() {
  return !!fired
}複製代碼

回調列表是否已經被觸發過。

回調列表觸發一次後 fired 就會變爲 true,用 !! 的目的是將 undefined 轉換爲 false 返回。

系列文章

  1. 讀Zepto源碼之代碼結構
  2. 讀 Zepto 源碼以內部方法
  3. 讀Zepto源碼之工具函數
  4. 讀Zepto源碼之神奇的$
  5. 讀Zepto源碼之集合操做
  6. 讀Zepto源碼之集合元素查找
  7. 讀Zepto源碼之操做DOM
  8. 讀Zepto源碼之樣式操做
  9. 讀Zepto源碼之屬性操做
  10. 讀Zepto源碼之Event模塊
  11. 讀Zepto源碼之IE模塊

參考

License

License: CC BY-NC-ND 4.0
License: CC BY-NC-ND 4.0

最後,全部文章都會同步發送到微信公衆號上,歡迎關注,歡迎提意見:

做者:對角另外一面

相關文章
相關標籤/搜索