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