由淺入深學習lodash的debounce函數

最近的面試中考到了debounce,函數防抖,筆試的時候答的不是特別好,下來好好研究了一下,從原理到優化,再到開源工具庫lodash的實現源碼,梳理了一番,現整理以下。面試

先簡單介紹一下debounce,從最簡單的一個場景入手,當用戶不斷點擊頁面,短期內頻繁的觸法點擊事件,只有在用戶觸法事件後的ns時間內,沒有再觸法事件,真正的監聽函數纔會執行,若是在這段時間內再次觸法了事件,就須要從新計算這個ns。app

debounce最主要的做用是把多個觸法事件的操做延遲到最後一次觸法執行,在性能上作了必定的優化。dom

不使用debounce

若是不使用debounce,那就會每一次點擊都會觸法事件的回調函數,這有時候對於性能是一種巨大的浪費(好比大量的增長dom元素)。或者當回調函數計算量很大的時候,甚至會致使阻塞。函數

window.addEventListener('click', function (event) {
  var p = document.createElement('p')
  p.innerHTML = 'trigger'
  document.body.appendChild(p)
})

頻繁觸法
能夠看出,每一次點擊都會觸法函數執行。工具

使用debounce

window.addEventListener('click', debounce(function (event) {
    var p = document.createElement('p')
    p.innerHTML = 'trigger'
    document.body.appendChild(p)
    return 'aaaa'
}, 500))

debounce優化
能夠看出,只有在最後一次點擊的500ms後,真正的函數func纔會觸法。性能

開始實現debounce

本篇文章的debounce實現主要參考了lodash庫,會從最基礎的實現開始,一步步完善它。
debounce的核心實現,就是要判斷每次觸法事件的時候,要不要執行真正的func優化

大致思路就是每次觸法事件都開啓一個延時的定時器,在定時器結束的時候對比與最後一次觸法事件時的時間差,若是時間差大於延遲的閾值,那麼就執行真正的func`。this

大體的結構以下spa

function debounce (func, wait) {
    var lastCallTime   // 最後一次觸法事件的時間
    var lastThis       // 做用域
    var lastArgs       // 參數
    var timerId        // 定時器對象
    wait = +wait || 0
    // 啓動定時器
    function startTimer (timerExpired, wait) {
        return setTimeout(timerExpired, wait)
    }
    
    // func函數執行   
    function invokeFunc () {
    
    }
    
    // 調用func函數的斷定條件 
    function shouldInvoke () {
    
    }
    
    //  定時器的回調函數 
    function timerExpired () {
        // 在這裏判斷觸法事件的時間差
    }
    
    // 要返回的函數
    function debounced (...args) {
    
    }
    
    return debounced
}

這就是基本的debounce函數的構成,下面邊解析,邊去一一填充這些函數,最後再對函數進行一步步的優化。code

debounced

每一次觸法事件的時候都會進入到這個函數,這個函數須要作這麼幾個事情。

  • 肯定做用域和參數
  • 更新觸法事件的時間,也就是lastCallTime
  • 啓動定時器 timerId
function debounced (...args) {
    const time = Date.now()
    lastThis = this
    lastArgs = args
    lastCallTime = time
    timerId = startTimer(timerExpired, wait)
}

startTimer

startTimer 就是啓動一個定時器,後續會有更多的拓展,因此封裝一個函數

function startTimer (timerExpired, wait) {
    return setTimeout(timerExpired, wait)
}

timerExpired

timerExpired 主要判斷是否執行func

function timerExpired () {
    const time = Date.now()
    if (shouldInvoke(time)) {
        return invokeFunc()
    }
}

shouldInvoke

shouldInvoke判斷每次事件觸法的時間差,若是大於閾值,那麼真正的func就會執行

function shouldInvoke (time) {
    return lastCallTime !== undefined && (time - lastCallTime >= wait)
}

invokeFunc

function invokeFunc () {
    timerId = undefined
    const args = lastArgs
    const thisArg = lastThis
    let result = func.apply(thisArg, args)
    lastArgs = lastThis = undefined
    return result
}

這樣,這個函數就寫完了。把每一步拆解開來,理解仍是相對容易的,再總結一下。每一次觸法事件,都開啓一個定時器timerId,而且會更新觸法事件的最後時間lastCallTime,在定時器的回調函數裏面,判斷回調函數的執行時間與lastCallTime的時間差,若是大於閾值,說明延遲時間到了,func執行,若是小於,就忽略。

優化

雖然實現了基本的debounce,但在擴展它的功能以前,看一看有沒有優化的空間,每一次觸法事件都開啓一個定時器是否是太浪費了。這裏可不能夠減小調用次數。

定時器調用頻率優化

把開啓定時器的邏輯放在timerExpired能夠大大減小定時器的數量。debounced開啓了第一次定時器後,debounced會忽略後面的定時器開啓,直到func執行以後(timerIdundefined),而在timerExpired裏面判斷若是func不知足觸發條件,那麼就開啓下一個定時器。

其實本質就是確保上一個定時器的回調不會觸法func了,纔會開啓下一個定時器。

優化代碼以下

function timerExpired () {
    const time = Date.now()
    if (shouldInvoke(time)) {
        return invokeFunc()
    }
    timerId = startTimer(timerExpired, wait)
}
function debounced (...args) {
    const time = Date.now()
    lastThis = this
    lastArgs = args
    lastCallTime = time
    if (timerId === undefined) {
        timerId = startTimer(timerExpired, wait)
    }
}

定時器時間的優化

timerExpired 中開啓的定時器

timerId = startTimer(timerExpired, wait)

延遲的時間是否必定爲wait呢,這是不必定的。
舉個例子,好比wait5,此時在某一個定時器的回調函數timerExpired檢測到上一次觸法事件的lastCallTime100,而Date.now()103,此時雖然103-100 = 3 < 5,要開啓下一次定時,但這個時候定時的時間爲 5 - 3 = 2就能夠了。這纔是精確的時間。

因此咱們須要把這個時間封裝成一個函數remainingWait

function remainingWait(time) {
    const timeSinceLastCall = time - lastCallTime
    const timeWaiting = wait - timeSinceLastCall
    return timeWaiting
}
function timerExpired () {
    const time = Date.now()
    if (shouldInvoke(time)) {
        return invokeFunc()
    }
    timerId = startTimer(timerExpired, remainingWait(time))
}

附上執行的流程圖

圖片描述

總結

這其實只是實現了一個basicDebounce,其實有的時候咱們須要在頻繁觸法事件的開始當即執行func,而忽略後面的觸法事件,這就須要加入參數控制,也就是lodash中的trailingleading,甚至二者同時存在,頭尾各執行一次,還有就是throttle函數節流,保證在一段時間內func至少執行一次,這就是lodash中的maxWait參數。下一篇文章會完善這些功能,屆時,一個完整的debounce纔是真正的實現了。

相關文章
相關標籤/搜索