摘要:定時器調(diào)用頻率優(yōu)化把開啟定時器的邏輯放在可以大大減少定時器的數(shù)量。舉個例子,比如為,此時在某一個定時器的回調(diào)函數(shù)檢測到上一次觸法事件的為,而為,此時雖然要開啟下一次定時,但這個時候定時的時間為就可以了。
最近的面試中考到了debounce,函數(shù)防抖,筆試的時候答的不是特別好,下來好好研究了一下,從原理到優(yōu)化,再到開源工具庫lodash的實現(xiàn)源碼,梳理了一番,現(xiàn)整理如下。
先簡單介紹一下debounce,從最簡單的一個場景入手,當用戶不斷點擊頁面,短時間內(nèi)頻繁的觸法點擊事件,只有在用戶觸法事件后的ns時間內(nèi),沒有再觸法事件,真正的監(jiān)聽函數(shù)才會執(zhí)行,如果在這段時間內(nèi)再次觸法了事件,就需要重新計算這個ns。
debounce最主要的作用是把多個觸法事件的操作延遲到最后一次觸法執(zhí)行,在性能上做了一定的優(yōu)化。
不使用debounce如果不使用debounce,那就會每一次點擊都會觸法事件的回調(diào)函數(shù),這有時候?qū)τ谛阅苁且环N巨大的浪費(比如大量的增加dom元素)?;蛘弋敾卣{(diào)函數(shù)計算量很大的時候,甚至會導(dǎo)致阻塞。
window.addEventListener("click", function (event) { var p = document.createElement("p") p.innerHTML = "trigger" document.body.appendChild(p) })
頻繁觸法
可以看出,每一次點擊都會觸法函數(shù)執(zhí)行。
window.addEventListener("click", debounce(function (event) { var p = document.createElement("p") p.innerHTML = "trigger" document.body.appendChild(p) return "aaaa" }, 500))
debounce優(yōu)化
可以看出,只有在最后一次點擊的500ms后,真正的函數(shù)func才會觸法。
本篇文章的debounce實現(xiàn)主要參考了lodash庫,會從最基礎(chǔ)的實現(xiàn)開始,一步步完善它。
debounce的核心實現(xiàn),就是要判斷每次觸法事件的時候,要不要執(zhí)行真正的func。
大體思路就是每次觸法事件都開啟一個延時的定時器,在定時器結(jié)束的時候?qū)Ρ扰c最后一次觸法事件時的時間差,如果時間差大于延遲的閾值,那么就執(zhí)行真正的func`。
大致的結(jié)構(gòu)如下
function debounce (func, wait) { var lastCallTime // 最后一次觸法事件的時間 var lastThis // 作用域 var lastArgs // 參數(shù) var timerId // 定時器對象 wait = +wait || 0 // 啟動定時器 function startTimer (timerExpired, wait) { return setTimeout(timerExpired, wait) } // func函數(shù)執(zhí)行 function invokeFunc () { } // 調(diào)用func函數(shù)的判定條件 function shouldInvoke () { } // 定時器的回調(diào)函數(shù) function timerExpired () { // 在這里判斷觸法事件的時間差 } // 要返回的函數(shù) function debounced (...args) { } return debounced }
這就是基本的debounce函數(shù)的構(gòu)成,下面邊解析,邊去一一填充這些函數(shù),最后再對函數(shù)進行一步步的優(yōu)化。
debounced每一次觸法事件的時候都會進入到這個函數(shù),這個函數(shù)需要做這么幾個事情。
確定作用域和參數(shù)
更新觸法事件的時間,也就是lastCallTime
啟動定時器 timerId
function debounced (...args) { const time = Date.now() lastThis = this lastArgs = args lastCallTime = time timerId = startTimer(timerExpired, wait) }startTimer
startTimer 就是啟動一個定時器,后續(xù)會有更多的拓展,所以封裝一個函數(shù)
function startTimer (timerExpired, wait) { return setTimeout(timerExpired, wait) }timerExpired
timerExpired 主要判斷是否執(zhí)行func
function timerExpired () { const time = Date.now() if (shouldInvoke(time)) { return invokeFunc() } }shouldInvoke
shouldInvoke判斷每次事件觸法的時間差,如果大于閾值,那么真正的func就會執(zhí)行
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 }
這樣,這個函數(shù)就寫完了。把每一步拆解開來,理解還是相對容易的,再總結(jié)一下。每一次觸法事件,都開啟一個定時器timerId,并且會更新觸法事件的最后時間lastCallTime,在定時器的回調(diào)函數(shù)里面,判斷回調(diào)函數(shù)的執(zhí)行時間與lastCallTime的時間差,如果大于閾值,說明延遲時間到了,func執(zhí)行,如果小于,就忽略。
優(yōu)化雖然實現(xiàn)了基本的debounce,但在擴展它的功能之前,看一看有沒有優(yōu)化的空間,每一次觸法事件都開啟一個定時器是不是太浪費了。這里可不可以減少調(diào)用次數(shù)。
定時器調(diào)用頻率優(yōu)化把開啟定時器的邏輯放在timerExpired可以大大減少定時器的數(shù)量。debounced開啟了第一次定時器后,debounced會忽略后面的定時器開啟,直到func執(zhí)行之后(timerId為undefined),而在timerExpired里面判斷如果func不滿足觸發(fā)條件,那么就開啟下一個定時器。
其實本質(zhì)就是確保上一個定時器的回調(diào)不會觸法func了,才會開啟下一個定時器。
優(yōu)化代碼如下
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) } }定時器時間的優(yōu)化
timerExpired 中開啟的定時器
timerId = startTimer(timerExpired, wait)
延遲的時間是否一定為wait呢,這是不一定的。
舉個例子,比如wait為5,此時在某一個定時器的回調(diào)函數(shù)timerExpired檢測到上一次觸法事件的lastCallTime為100,而Date.now()為103,此時雖然103-100 = 3 < 5,要開啟下一次定時,但這個時候定時的時間為 5 - 3 = 2就可以了。這才是精確的時間。
所以我們需要把這個時間封裝成一個函數(shù)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)) }
附上執(zhí)行的流程圖
總結(jié)這其實只是實現(xiàn)了一個basicDebounce,其實有的時候我們需要在頻繁觸法事件的開始立即執(zhí)行func,而忽略后面的觸法事件,這就需要加入?yún)?shù)控制,也就是lodash中的trailing和leading,甚至兩者同時存在,頭尾各執(zhí)行一次,還有就是throttle函數(shù)節(jié)流,保證在一段時間內(nèi)func至少執(zhí)行一次,這就是lodash中的maxWait參數(shù)。下一篇文章會完善這些功能,屆時,一個完整的debounce才是真正的實現(xiàn)了。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/95495.html
摘要:首先重置防抖函數(shù)最后調(diào)用時間,然后去觸發(fā)一個定時器,保證后接下來的執(zhí)行。這就避免了手動管理定時器。 ??之前遇到過一個場景,頁面上有幾個d3.js繪制的圖形。如果調(diào)整瀏覽器可視區(qū)大小,會引發(fā)圖形重繪。當圖中的節(jié)點比較多的時候,頁面會顯得異??D。為了限制類似于這種短時間內(nèi)高頻率觸發(fā)的情況,我們可以使用防抖函數(shù)。 ??實際開發(fā)過程中,這樣的情況其實很多,比如: 頁面的scroll事件 ...
摘要:最簡單的案例以最簡單的情景為例在某一時刻點只調(diào)用一次函數(shù),那么將在時間后才會真正觸發(fā)函數(shù)。后續(xù)我們會逐漸增加黑色鬧鐘出現(xiàn)的復(fù)雜度,不斷去分析紅色鬧鐘的位置。 序 相比網(wǎng)上教程中的 debounce 函數(shù),lodash 中的 debounce 功能更為強大,相應(yīng)的理解起來更為復(fù)雜; 解讀源碼一般都是直接拿官方源碼來解讀,不過這次我們采用另外的方式:從最簡單的場景開始寫代碼,然后慢慢往源碼...
摘要:譯通過實例講解和防抖與節(jié)流源碼中推薦的文章,為了學習英語,翻譯了一下原文鏈接作者本文來自一位倫敦前端工程師的技術(shù)投稿。首次或立即你可能發(fā)現(xiàn)防抖事件在等待觸發(fā)事件執(zhí)行,直到事件都結(jié)束后它才執(zhí)行。 [譯]通過實例講解Debouncing和Throtting(防抖與節(jié)流) lodash源碼中推薦的文章,為了學習(英語),翻譯了一下~ 原文鏈接 作者:DAVID CORBACHO 本文來自一位...
摘要:可以看下面的栗子這個圖中圖中每個小格大約,右邊有原生事件與節(jié)流去抖插件的與事件。即如果有連續(xù)不斷的觸發(fā),每執(zhí)行一次,用在每隔一定間隔執(zhí)行回調(diào)的場景。執(zhí)行啦打印執(zhí)行啦打印執(zhí)行啦節(jié)流按照上面的說明,節(jié)流就是連續(xù)多次內(nèi)的操作按照指定的間隔來執(zhí)行。 一般在項目中我們會對input、scroll、resize等事件進行節(jié)流控制,防止事件過多觸發(fā),減少資源消耗;在vue的官網(wǎng)的例子中就有關(guān)于lod...
摘要:背景需要包寫起來爽,然而如果遇到?jīng)]有現(xiàn)成的化的工具函數(shù),就需要自己想辦法弄出一份類型聲明文件了。最為重要的是,這種遷移方面我們可以隨意自定義化中所需要的工具函數(shù),遷移粒度都可以由自己控制。 1、背景 1.1、需要 TS 包 TypeScript 寫起來爽,然而如果遇到?jīng)]有現(xiàn)成的 TS 化的工具函數(shù),就需要自己想辦法弄出一份類型聲明文件了。 前兩天要寫的小工具庫(Typescript 語...
閱讀 1453·2021-10-08 10:05
閱讀 3118·2021-09-26 10:10
閱讀 923·2019-08-30 15:55
閱讀 530·2019-08-26 11:51
閱讀 473·2019-08-23 18:10
閱讀 3898·2019-08-23 15:39
閱讀 689·2019-08-23 14:50
閱讀 802·2019-08-23 14:46