最近的面試中考到了debounce
,函數防抖,筆試的時候答的不是特別好,下來好好研究了一下,從原理到優化,再到開源工具庫lodash
的實現源碼,梳理了一番,現整理以下。面試
先簡單介紹一下debounce
,從最簡單的一個場景入手,當用戶不斷點擊頁面,短期內頻繁的觸法點擊事件,只有在用戶觸法事件後的n
s時間內,沒有再觸法事件,真正的監聽函數纔會執行,若是在這段時間內再次觸法了事件,就須要從新計算這個n
s。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
執行以後(timerId
爲undefined
),而在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
呢,這是不必定的。
舉個例子,好比wait
爲5
,此時在某一個定時器的回調函數timerExpired
檢測到上一次觸法事件的lastCallTime
爲100
,而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
中的trailing
和leading
,甚至二者同時存在,頭尾各執行一次,還有就是throttle
函數節流,保證在一段時間內func
至少執行一次,這就是lodash
中的maxWait
參數。下一篇文章會完善這些功能,屆時,一個完整的debounce
纔是真正的實現了。