讀Zepto源碼之Callbacks模塊

来源:http://www.cnblogs.com/hefty/archive/2017/07/23/7223750.html
-Advertisement-
Play Games

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

基礎用法

我用 jQueryZepto 的時間比較短,之前也沒有直接用過 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.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

作者:對角另一面


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 近期在學習【時間管理】方面的課程,其中有一期講了蕃茄工作法,發現是個好多東西。蕃茄工作法核心思想就是:工作25分鐘,休息5分鐘。如果您好瞭解更多可以自行度娘。 在加上本人是一個程式猿,就想用程式的方式來表達對此工作法的敬意。因此就產生了用vue實現一個tomato timer的想法。演示地址 一、v ...
  • 一、首先是喜聞樂見的position方法,經典且萬能,用法如下: 不需要水平居中可以去掉left和margin-left。 劃重點:需要父元素和子元素都定義寬高,自適應是不可能自適應的,這輩子都不可能自適應的。 二、 display:table-cell能夠使大小不固定的元素實現垂直居中佈局,先來一 ...
  • //首先寫一個導航欄樣式 .nav{ width:560px; height: 50px; font:bold 0/50px Arial; text-align:center; margin:40px auto 0; background: #f65f57; border-radius:10px;/ ...
  • 我們扒取到網站源碼很多時候發現路徑是採用相對路徑,這時候我們就需要採用base標簽了,用法非常簡單, <base href="我們扒取網站的功能變數名稱"/> 這時相對路徑就相對於這個網站功能變數名稱了,就可以正常顯示效果了 ...
  • js獲取單選按鈕的值 js獲取覆選框的值 ...
  • [1]安裝 [2]命令行 [3]標簽 [4]內容 [5]屬性 [6]註釋 [7]代碼 [8]條件 [9]迴圈 [10]混入 [11]包含 [12]繼承 [13]簡易模板 ...
  • 在平時的開發項目中,難免接觸前端的知識,需要寫介面,有時候用到js中的ajax跨越請求,總結了ajax的寫法。 開始之前,需要準備兩個文件,ajax.php ;ajax.html 1.ajax的基本步驟(ajax.php) ajax,有同步非同步的區別?非同步:把小弟派出去了,什麼時候回來,什麼時候處理 ...
  • React在Github上已經有接近70000的 star 數了,是目前最熱門的前端框架。而我學習React也有一段時間了,現在就開始用 React+Redux 進行實戰! 上回說到使用Redux-saga 管理 Redux 應用非同步操作,應用還是只有一個首頁.現在開始構建一個新的投稿頁面並使用 R ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...