讀Zepto源碼之Callbacks模塊

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 callbackonce: 回調至多隻能觸發一次
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) // 刪除 ccallbacks.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 = truefiringIndex = firingStart || 0firingStart = 0firingLength = list.lengthfiring = 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.lengthelse 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 返回。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章