摘要:定時器調用頻率優化把開啟定時器的邏輯放在可以大大減少定時器的數量。舉個例子,比如為,此時在某一個定時器的回調函數檢測到上一次觸法事件的為,而為,此時雖然要開啟下一次定時,但這個時候定時的時間為就可以了。
最近的面試中考到了debounce,函數防抖,筆試的時候答的不是特別好,下來好好研究了一下,從原理到優化,再到開源工具庫lodash的實現源碼,梳理了一番,現整理如下。
先簡單介紹一下debounce,從最簡單的一個場景入手,當用戶不斷點擊頁面,短時間內頻繁的觸法點擊事件,只有在用戶觸法事件后的ns時間內,沒有再觸法事件,真正的監聽函數才會執行,如果在這段時間內再次觸法了事件,就需要重新計算這個ns。
debounce最主要的作用是把多個觸法事件的操作延遲到最后一次觸法執行,在性能上做了一定的優化。
不使用debounce如果不使用debounce,那就會每一次點擊都會觸法事件的回調函數,這有時候對于性能是一種巨大的浪費(比如大量的增加dom元素)。或者當回調函數計算量很大的時候,甚至會導致阻塞。
window.addEventListener("click", function (event) { var p = document.createElement("p") p.innerHTML = "trigger" document.body.appendChild(p) })
頻繁觸法
可以看出,每一次點擊都會觸法函數執行。
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實現主要參考了lodash庫,會從最基礎的實現開始,一步步完善它。
debounce的核心實現,就是要判斷每次觸法事件的時候,要不要執行真正的func。
大體思路就是每次觸法事件都開啟一個延時的定時器,在定時器結束的時候對比與最后一次觸法事件時的時間差,如果時間差大于延遲的閾值,那么就執行真正的func`。
大致的結構如下
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函數的構成,下面邊解析,邊去一一填充這些函數,最后再對函數進行一步步的優化。
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才是真正的實現了。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/95495.html
摘要:首先重置防抖函數最后調用時間,然后去觸發一個定時器,保證后接下來的執行。這就避免了手動管理定時器。 ??之前遇到過一個場景,頁面上有幾個d3.js繪制的圖形。如果調整瀏覽器可視區大小,會引發圖形重繪。當圖中的節點比較多的時候,頁面會顯得異常卡頓。為了限制類似于這種短時間內高頻率觸發的情況,我們可以使用防抖函數。 ??實際開發過程中,這樣的情況其實很多,比如: 頁面的scroll事件 ...
摘要:最簡單的案例以最簡單的情景為例在某一時刻點只調用一次函數,那么將在時間后才會真正觸發函數。后續我們會逐漸增加黑色鬧鐘出現的復雜度,不斷去分析紅色鬧鐘的位置。 序 相比網上教程中的 debounce 函數,lodash 中的 debounce 功能更為強大,相應的理解起來更為復雜; 解讀源碼一般都是直接拿官方源碼來解讀,不過這次我們采用另外的方式:從最簡單的場景開始寫代碼,然后慢慢往源碼...
摘要:譯通過實例講解和防抖與節流源碼中推薦的文章,為了學習英語,翻譯了一下原文鏈接作者本文來自一位倫敦前端工程師的技術投稿。首次或立即你可能發現防抖事件在等待觸發事件執行,直到事件都結束后它才執行。 [譯]通過實例講解Debouncing和Throtting(防抖與節流) lodash源碼中推薦的文章,為了學習(英語),翻譯了一下~ 原文鏈接 作者:DAVID CORBACHO 本文來自一位...
摘要:可以看下面的栗子這個圖中圖中每個小格大約,右邊有原生事件與節流去抖插件的與事件。即如果有連續不斷的觸發,每執行一次,用在每隔一定間隔執行回調的場景。執行啦打印執行啦打印執行啦節流按照上面的說明,節流就是連續多次內的操作按照指定的間隔來執行。 一般在項目中我們會對input、scroll、resize等事件進行節流控制,防止事件過多觸發,減少資源消耗;在vue的官網的例子中就有關于lod...
摘要:背景需要包寫起來爽,然而如果遇到沒有現成的化的工具函數,就需要自己想辦法弄出一份類型聲明文件了。最為重要的是,這種遷移方面我們可以隨意自定義化中所需要的工具函數,遷移粒度都可以由自己控制。 1、背景 1.1、需要 TS 包 TypeScript 寫起來爽,然而如果遇到沒有現成的 TS 化的工具函數,就需要自己想辦法弄出一份類型聲明文件了。 前兩天要寫的小工具庫(Typescript 語...
閱讀 1416·2021-10-08 10:05
閱讀 3060·2021-09-26 10:10
閱讀 883·2019-08-30 15:55
閱讀 504·2019-08-26 11:51
閱讀 441·2019-08-23 18:10
閱讀 3849·2019-08-23 15:39
閱讀 658·2019-08-23 14:50
閱讀 767·2019-08-23 14:46