摘要:最簡單的案例以最簡單的情景為例在某一時刻點(diǎn)只調(diào)用一次函數(shù),那么將在時間后才會真正觸發(fā)函數(shù)。后續(xù)我們會逐漸增加黑色鬧鐘出現(xiàn)的復(fù)雜度,不斷去分析紅色鬧鐘的位置。
序
相比網(wǎng)上教程中的 debounce 函數(shù),lodash 中的 debounce 功能更為強(qiáng)大,相應(yīng)的理解起來更為復(fù)雜;
解讀源碼一般都是直接拿官方源碼來解讀,不過這次我們采用另外的方式:從最簡單的場景開始寫代碼,然后慢慢往源碼上來靠攏,循序漸進(jìn)來實現(xiàn) lodash 中的 debounce 函數(shù),從而更深刻理解官方 debounce 源碼的用意。
為了減少純代碼帶來的晦澀感,本文以圖例來輔助講解,一方面這樣能減少源碼閱讀帶來的枯燥感,同時也讓后續(xù)回憶源碼內(nèi)容更加的具體形象。(記住圖的內(nèi)容,后續(xù)再寫出源碼也變得簡單些)
在本文的末尾還會附上簡易的 debounce & throttle 的實現(xiàn)的代碼片段,方便平時快速用在簡單場景中,免去引用 lodash 庫。
本文屬于源碼解讀類型的文章,對 debounce 還不熟悉的讀者建議先通過參考文章(在文末)了解該函數(shù)的概念和用法。1、用圖例解析 debounce 源碼
附源碼 debounce: https://github.com/boycgit/ts...
首先搬出 debounce(防抖)函數(shù)的概念:函數(shù)在 wait 秒內(nèi)只執(zhí)行一次,若這 wait 秒內(nèi),函數(shù)高頻觸發(fā),則會重新計算時間。
看似簡單一句話,內(nèi)含乾坤。為方便行文敘述,約定如下術(shù)語:
假定我們要對 func 函數(shù)進(jìn)行 debounce 處理,經(jīng) debounced 后的返回值我們稱之為 debounced func
wait 表示傳入防抖函數(shù)的時間
time 表示當(dāng)前時間戳
lastCallTime 表示上一次調(diào)用 debounced func 函數(shù)的時間
lastInvokeTime 表示上一次 func 函數(shù)執(zhí)行的時間
result 是每次調(diào)用 debounced func 函數(shù)的返回值
用 time 表示當(dāng)前時間
本文將搭配圖例 + 程序代碼的方式,將上述概念具象到圖中。
2、最簡單的案例以最簡單的情景為例:在某一時刻點(diǎn)只調(diào)用一次 debounced func 函數(shù),那么將在 wait 時間后才會真正觸發(fā) func 函數(shù)。
將這個情景形成一幅圖例,最終繪制出的圖如下所示:
下面我們詳細(xì)講解這幅圖的產(chǎn)生過程,其實不難,基本上看一遍就懂。
首先繪制在圖中放置一個黑色鬧鐘表示用戶調(diào)用 debounced func 函數(shù):(同時用 lastCallTime 標(biāo)示出最近一次調(diào)用 debounced func 的時間)
同時在距離該黑色鬧鐘 wait 處放置一個藍(lán)色鬧鐘,表示setTimout(..., wait),該藍(lán)色鬧鐘表示未來當(dāng)代碼運(yùn)行到該時間點(diǎn)時,需要做一些判斷:
為了標(biāo)示出表示程序當(dāng)前運(yùn)行的進(jìn)度(當(dāng)前時間戳),我們用橙紅色滑塊來表示:
當(dāng)紅色滑塊到達(dá)該藍(lán)色鬧鐘處的時候,藍(lán)色鬧鐘會進(jìn)行判斷:因為當(dāng)前滑塊距離最近的黑色鬧鐘的時間差為 wait:
故而做出判斷(依據(jù) debounce 函數(shù)的功能定義):需要觸發(fā)一次 func 函數(shù),我們用紅色鬧鐘來表示 func 函數(shù)的調(diào)用,所以就放置一個紅色鬧鐘
很顯然藍(lán)色和紅色鬧鐘重疊起來的。
同時我們給紅色鬧鐘標(biāo)上 lastInvokeTime,記錄最近一次調(diào)用 func 的時間:
注意 lastInvokeTime 和 lastCallTime 的區(qū)別,兩者含義是不一樣的
這樣我們就完成了最簡單場景下 debounce 圖例的繪制,簡單易懂。
后續(xù)我們會逐漸增加黑色鬧鐘出現(xiàn)的復(fù)雜度,不斷去分析紅色鬧鐘的位置。這樣就能將理解 debounce 源碼的問題轉(zhuǎn)換成“根據(jù)圖上黑色鬧鐘的位置,請畫出紅色鬧鐘位置”的問題,而分析紅色鬧鐘位置的過程中也就是理解 debounce 源碼的過程;
用圖例方式輔助理解源碼的方式可以減少源碼閱讀帶來的枯燥感,同時后續(xù)回憶源碼內(nèi)容起來也更加具體形象。
為避免后續(xù)寫文章到處解釋圖中元素的概念含義,這里不妨先羅列出來,如果閱讀過程中忘記到這里回憶一下也會方便許多:
橫線代表時間軸,橙紅色滑塊代表當(dāng)前時間 time
每個黑色箭頭表示 debounced func 函數(shù)的調(diào)用
黑色鬧鐘表示調(diào)用 debounced func 函數(shù)時的時間,最后一次黑色鬧鐘上標(biāo)上 lastCallTime,表示最近一次調(diào)用的時間戳;
紅色鬧鐘表示調(diào)用 func 函數(shù)的時間,最后一次紅色鬧鐘上標(biāo)上 lastInvokeTime,表示最近一次調(diào)用的時間戳;
此外還有一個藍(lán)色鬧鐘,表示 setTimeout 時間戳(用來規(guī)劃 func 函數(shù)執(zhí)行的時間),每次時間軸上的橙紅色滑塊到這個時間點(diǎn)就要做判斷:是執(zhí)行 func 或者推遲藍(lán)色鬧鐘位置
有關(guān)藍(lán)色鬧鐘,這里有兩個注意點(diǎn):
時間軸上最多同時只有一個藍(lán)色鬧鐘;
只有在第一次調(diào)用 debounced func 函數(shù)時才會在 wait 時間后放置藍(lán)色鬧鐘,后續(xù)鬧鐘的出現(xiàn)位置就由藍(lán)色鬧鐘自己決策(下文會舉例說明)
3、有 N 多個黑色鬧鐘的場景現(xiàn)在我們來一個稍微復(fù)雜的場景:
假如在 wait 時間內(nèi)(記住這個前提條件)調(diào)用 n 次 debounced func 函數(shù),如下所示:
第一次調(diào)用 debounced func 函數(shù)會在 wait 時間后放置藍(lán)色鬧鐘(只有第一次調(diào)用會放置藍(lán)色鬧鐘,后續(xù)鬧鐘的位置由藍(lán)色鬧鐘自己決策):
以上就是描述,那么問題來了:請問紅色鬧鐘應(yīng)該出現(xiàn)在時間軸哪個位置?
3.1、分析紅色鬧鐘出現(xiàn)的位置我們只關(guān)注最后一個黑色鬧鐘,并假設(shè)藍(lán)色鬧鐘距離該黑色鬧鐘時間間隔為 x:
那么第一個黑色鬧鐘和最后一個黑色鬧鐘的時間間隔是 wait - x:
接下來我們關(guān)注橙紅色滑塊(即當(dāng)前時間time)到達(dá)藍(lán)色鬧鐘的時,藍(lán)色鬧鐘開始做決策:計算可知 x < wait,此時藍(lán)色鬧鐘決定不放置紅色鬧鐘(即不觸發(fā) func),而是將藍(lán)色鬧鐘往后挪了挪,挪動距離為 wait - x,調(diào)整完之后的藍(lán)色鬧鐘位置如下:
之所以挪 wait - x 的距離,是因為挪完后的藍(lán)色鬧鐘距離最后一個黑色鬧鐘恰好為 wait 間隔(從而保證 debounce 函數(shù)至少間隔 wait 時間 才觸發(fā)的條件):
從挪移之后開始,到下一次橙色鬧鐘再次遇到藍(lán)色鬧鐘這段期間,我們暫且稱之為 ”藍(lán)色決策間隔期“(請忍耐這抽象的名稱,畢竟我想了好久),藍(lán)色鬧鐘基于此間隔期的內(nèi)容來進(jìn)行決策,只有兩種決策:
如果在”藍(lán)色決策間隔期“內(nèi)沒有黑鬧鐘出現(xiàn),那么紅色滑塊達(dá)到藍(lán)色鬧鐘的時候,藍(lán)色鬧鐘計算獲知當(dāng)前藍(lán)色鬧鐘距離上一個黑色鬧鐘的時間間隔不少于 wait(time - lastCallTime >= wait),那就會放置紅色鬧鐘(即調(diào)用 func),目標(biāo)達(dá)成;
如果在”藍(lán)色決策間隔期“內(nèi)仍舊有黑鬧鐘出現(xiàn),那么當(dāng)橙紅色滑塊到達(dá)藍(lán)色鬧鐘時,藍(lán)色鬧鐘又會重新計算與該間隔期內(nèi)最后一只黑色鬧鐘的距離 y,隨后 又會往后挪動位置 wait-y,再一次保證藍(lán)色鬧鐘距離最后一個黑色鬧鐘恰好為 wait 間隔 —— 沒錯,又形成了新的 ”藍(lán)色決策間隔期“;那接下去的分析就又回到了 這里兩點(diǎn)(即遞歸決策),直到能放置到紅鬧鐘為止。
從上我們可以看到,藍(lán)色鬧鐘一直保持 ”紳士“ 風(fēng)范,隨著黑色鬧鐘的逼近,藍(lán)色鬧鐘一直保持”克制“態(tài)度,不斷調(diào)整自己的位置,讓調(diào)整后的位置總是和最后一個黑色鬧鐘保持 wait 的距離。
3.2、用代碼描述圖例過程我們用代碼將上述的過程描述出來,就是下面這個樣子:
function debounce(func, wait, options) { var lastArgs, lastThis, result, timerId, lastCallTime, lastInvokeTime = 0, trailing = true; wait = toNumber(wait) || 0; // 紅色滑塊達(dá)到藍(lán)色鬧鐘時,藍(lán)色鬧鐘根據(jù)條件作出決策 function timerExpired() { var time = now(); // 決策 1: 滿足放置紅色鬧鐘的條件,則放置紅鬧鐘 if (shouldInvoke(time)) { return trailingEdge(time); } // 否則,決策 2:將藍(lán)色鬧鐘再往后挪 `wait-x` 位置,形成 ”藍(lán)色決策間隔期“ timerId = setTimeout(timerExpired, remainingWait(time)); } // === 以下是具體決策中的函數(shù)實現(xiàn) ==== // 做出 ”應(yīng)當(dāng)放置紅色鬧鐘“ 的決策的條件:藍(lán)色鬧鐘和最后一個黑色鬧鐘的間隔不小于 wait 間隔 function shouldInvoke(time) { var timeSinceLastCall = time - lastCallTime; return ( timeSinceLastCall >= wait ); } // 具體函數(shù):放置紅色鬧鐘 function trailingEdge(time) { timerId = undefined; if (trailing && lastArgs) { return invokeFunc(time); } lastArgs = lastThis = undefined; return result; } // 具體函數(shù) - 子函數(shù):在時間軸上放置紅鬧鐘 function invokeFunc(time) { var args = lastArgs, thisArg = lastThis; lastArgs = lastThis = undefined; lastInvokeTime = time; result = func.apply(thisArg, args); return result; } // 具體函數(shù):計算讓藍(lán)色鬧鐘往后挪 wait-x 位置 function remainingWait(time) { var timeSinceLastCall = time - lastCallTime, timeWaiting = wait - timeSinceLastCall; return timeWaiting ; } // ============== // 主流程:讓紅色滑塊在時間軸上前進(jìn)(即 debounced func 函數(shù)的執(zhí)行) function debounced() { var time = now(); lastArgs = arguments; lastThis = this; lastCallTime = time; if (timerId === undefined) { timerId = setTimeout(timerExpired, wait); } return result; } return debounced; }
這部分代碼還請不要略過,因為代碼是從debounce源碼中整理過來,除了函數(shù)順序略有調(diào)整外,源碼風(fēng)格保持原有的,相當(dāng)于直接閱讀源碼。每個函數(shù)都有注釋,對比著圖例閱讀下來相信讀完會有收獲的。
上述這份代碼已經(jīng)包含了 debounce 源碼的核心骨架,接下來我們繼續(xù)擴(kuò)展場景,將源碼內(nèi)容豐滿起來。
4、豐富功能特性 4.1、支持 leading 特性leading 功能簡單理解就是,在第一次(注意這個條件)放下黑色鬧鐘的時候:
立即放置紅鬧鐘,同時在
此后 wait 處放置方式藍(lán)色鬧鐘(注:第一次放下黑色鬧鐘的時候,按理說也會在 wait 處放下藍(lán)色鬧鐘,考慮既然 leading 也有這種操作,那么就不多此一舉。記?。赫麄€時間軸上最多只能同時有一個藍(lán)色鬧鐘)
用圖說話:
第一次放置黑色鬧鐘的時候,會疊加上紅色鬧鐘(當(dāng)然這個紅色鬧鐘上會標(biāo)示 lastInvokeTime),另外在 wait 間隔后會有藍(lán)色鬧鐘。其他流程和之前案例分析一樣。
在代碼層面,我們給剛才的 debounce 函數(shù)添加 leading 功能(通過 options.leading 開啟)、新增一個 leadingEdge 方法后,再微調(diào)剛才的代碼:
function debounce(func, wait, options) { ... var leading = false; // 默認(rèn)不開啟 leading = !!options.leading; // 通過 options.leading 開啟 ... // 首先:新增執(zhí)行 leading 處的操作的函數(shù) function leadingEdge(time) { lastInvokeTime = time; // 設(shè)置 lastInvokeTime 時間標(biāo)簽 timerId = setTimeout(timerExpired, wait); // 同時在此后 `wait` 處放置一個藍(lán)色鬧鐘 return leading ? invokeFunc(time) : result; // 如果開啟,直接放置紅色鬧鐘;否則直接返回 result 數(shù)值 } ... // 其次:給放置紅色鬧鐘新增一種條件 function shouldInvoke(time) { ... return ( lastCallTime === undefined || // 初次執(zhí)行時 timeSinceLastCall >= wait // 或者前面分析的條件,兩次 `debounced func` 調(diào)用間隔大于 wait ); } // 注意:放置完紅色鬧鐘后,記得要清空 timerId,相當(dāng)于清空時間軸上藍(lán)色鬧鐘; function trailingEdge(time) { timerId = undefined; ... } // 最后:leading 邊界調(diào)用 function debounced(){ ... var isInvoking = shouldInvoke(time); // 判斷是否可以放置紅色鬧鐘 ... if (isInvoking) { // 如果可以放置紅色鬧鐘 if (timerId === undefined) { // 且當(dāng)時間軸上沒有藍(lán)色鬧鐘 // 執(zhí)行 leading 邊界處操作(放置紅色鬧鐘 或 直接返 result) return leadingEdge(lastCallTime); } } ... return result; } return debounced; }4.2、支持 maxWait 特性
要理解這個 maxWait 特性,我們先看一種特殊情況,在 {leading: false} 下, 時間軸上我們很密集地放置黑色鬧鐘:
按之前的所述規(guī)則,我們的藍(lán)色鬧鐘一直保持紳士態(tài)度,隨著黑色鬧鐘的逼近,藍(lán)色鬧鐘將不斷將調(diào)整自己的位置,讓自己調(diào)整后的位置總是和最后一個黑色鬧鐘保持 wait 的距離:
那么在這種情況下,如果黑色鬧鐘一直保持這種密集放置狀態(tài),理論上就紅色鬧鐘就沒有機(jī)會出現(xiàn)在時間軸上。
那在這種情況下能否實現(xiàn)一個功能,無論黑色鬧鐘多么密集,時間軸上最多隔 maxWait 時間就出現(xiàn)紅色鬧鐘,就像下圖那樣:
有了這個功能屬性后,藍(lán)色鬧鐘從此 ”變得堅強(qiáng)“,也有了 "底線",縱使黑色鬧鐘的不斷逼近,也會堅守 maxWait 底線,到點(diǎn)就放置紅色鬧鐘。
實現(xiàn)該特性的大致思路如下:
maxWait 是與 lastInvokeTime 共同協(xié)作
在藍(lán)色鬧鐘計算后退距離時,maxWait 發(fā)揮作用;在沒有 maxWait 的時候,是按上一次黑色鬧鐘進(jìn)行測距,保證調(diào)整后的藍(lán)色鬧鐘和黑色鬧鐘保持 wait 的距離;而在有了 maxWait 后,藍(lán)色鬧鐘調(diào)整距離還會考慮上一次紅色鬧鐘的位置,保持調(diào)整后鬧鐘的位置和紅色鬧鐘距離不能超過 maxWait,這就是底線了,到了一定程度,就算黑色鬧鐘在逼近,藍(lán)色鬧鐘也不會 ”退縮“:
從代碼層面上看, maxWait 具體是在 remainingWait 方法 和 shouldInvoke 中發(fā)揮作用的:
function debounce(func, wait, options) { ... var lastInvokeTime = 0; // 初始化 var maxing = false; // 默認(rèn)沒有底線 maxing = "maxWait" in options; maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait; // 從 options.maxWait 中獲取底線數(shù)值 ... // 首先,在在藍(lán)色鬧鐘決策后退多少距離時,maxWait 發(fā)揮了作用 function remainingWait(time) { var timeSinceLastCall = time - lastCallTime, timeSinceLastInvoke = time - lastInvokeTime, timeWaiting = wait - timeSinceLastCall; // 在這里發(fā)揮作用,保持底線 return maxing ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting; } ... // 其次:針對 `maxWait`,給放置紅色鬧鐘新增一種可能條件 function shouldInvoke(time) { ... var timeSinceLastInvoke = time - lastInvokeTime; // 獲取距離上一次紅色鬧鐘的時間間隔 return ( lastCallTime === undefined || // 初次執(zhí)行時 timeSinceLastCall >= wait || // 或者前面分析的條件,兩次 `debounced func` 調(diào)用間隔大于 wait (maxing && timeSinceLastInvoke >= maxWait) // 兩次紅色鬧鐘間隔超過 maxWait ); } // 最后:leading 邊界調(diào)用 function debounced(){ ... var isInvoking = shouldInvoke(time); // 判斷是否可以放置紅色鬧鐘的條件 ... if (isInvoking) { // 如果可以放置紅色鬧鐘 ... // 邊界情況的處理,保證在緊 loop 中能正常保持觸發(fā) if (maxing) { timerId = setTimeout(timerExpired, wait); return invokeFunc(lastCallTime); } } ... return result; } return debounced; }
因此,maxWait 能夠讓紅色鬧鐘保證在 maxWait 間隔內(nèi)至少出現(xiàn) 1 次;
4.3、支持 cancel / flush 方法這兩個函數(shù)是為了能隨時控制 debounce 的緩存狀態(tài);
其中 cancel 方法源碼如下:
// 取消防抖 function cancel() { if (timerId !== undefined) { clearTimeout(timerId); } lastInvokeTime = 0; lastArgs = lastCallTime = lastThis = timerId = undefined; }
調(diào)用該方法,相當(dāng)于直接在時間軸上去除藍(lán)色鬧鐘,這樣紅色方塊(時間)就永遠(yuǎn)遇見不了藍(lán)色鬧鐘,那樣也就不會有放置紅色鬧鐘的可能了。
其中 flush 方法源碼如下:
function flush() { return timerId === undefined ? result : trailingEdge(now()); }
非常直觀,調(diào)用該方法相當(dāng)于直接在時間軸上放置紅色鬧鐘。
至此,我們已經(jīng)完整實現(xiàn)了 lodash 的 debounce 函數(shù),也就相當(dāng)于閱讀了一遍其源碼。
5、實現(xiàn) throttle 函數(shù)在完成上面 debounce 功能和特性后(尤其是 maxWait 特性),就能借助 debounce 實現(xiàn) throttle 函數(shù)了。
看 throttle 源碼 就能明白:
function throttle(func, wait, options) { var leading = true, trailing = true; // ... return debounce(func, wait, { "leading": leading, "maxWait": wait, "trailing": trailing }); }
所以在 lodash 中,只需要 debounce 函數(shù)即可,throttle 相當(dāng)于 ”充話費(fèi)“ 送的。
至此,我們已經(jīng)解讀完 lodash 中的 debounce & throttle 函數(shù)源碼;
最后附帶一張 lodash 實現(xiàn)執(zhí)行效果圖,用來自測是否真的理解通透:
注:此圖取自于文章《 聊聊lodash的debounce實現(xiàn)》6、小結(jié)
在前端領(lǐng)域的性能優(yōu)化手段中,防抖(debounce)和節(jié)流(throttle)是必備的技能,網(wǎng)上隨便一搜就有很多文章去分析解釋,不乏優(yōu)秀的文章使用 圖文混排 + 類比方式 深入淺出探討這兩函數(shù)的用法和使用場景(見文末的參考文檔)。
那我為什么還要寫這一篇文章?
緣起前兩天手動將 lodash 中的 debounce 和 throttle 兩個函數(shù) TS 化的需求,而平時我也只是使用并沒有在意它們真正的實現(xiàn)原理,因此在遷移過程我順帶閱讀了一番 lodash 中這兩個函數(shù)的源碼。
具體原因和遷移過程請移步《技巧 - 快速 TypeScript 化 lodash 中的 throttle & debounce 函數(shù)》
本文嘗試提供了另一個視角去解讀,通過時間軸 + 鬧鐘圖例 + 代碼的方式來解讀 lodash 中的 debounce & throttle 源碼;
整個流程下來只要理解了黑色、藍(lán)色、紅色這 3 種鬧鐘的關(guān)系,那么憑著理解力去實現(xiàn)簡版 lodash 的 debounce 函數(shù)并非難事。
當(dāng)然上述的敘述中,略過了很多細(xì)節(jié)和存在性的判斷(諸如 timeId 的存在性判斷、isInvoking的出現(xiàn)位置等),省略這主要是為了降低源碼閱讀的難度;(實際中這些細(xì)節(jié)的處理有時候反而很重要,是代碼健壯性不可或缺的一部分)
希望本文能對讀者理解 lodash 中的 debounce & throttle 源碼有些許的幫助,歡迎隨時關(guān)注微信公眾號或者技術(shù)博客留言交流。
【附】代碼片段如果在你僅僅需要應(yīng)付簡單的一些場景,也可以直接使用下方的代碼片段。
A. 簡易 debounce - 只實現(xiàn) trailing 情況防抖函數(shù)的概念:函數(shù)在 n 秒內(nèi)只執(zhí)行一次,若這 n 秒內(nèi),函數(shù)高頻觸發(fā),則會重新計算時間。
將這段話翻譯成代碼,你會發(fā)現(xiàn)并不難:
//防抖代碼最簡單的實現(xiàn) function debounce(func, wait) { let timerId, result; return function() { if(timerId){ clearTimeout(timerId); // 每次觸發(fā) 都清除當(dāng)前timer,重新設(shè)置時間 } timerId = setTimeout(function(){ result = func.apply(this, arguments); }, wait); return result; } }
debounce 返回閉包(匿名函數(shù))
假如調(diào)用該閉包兩次:
如果調(diào)用兩次間隔 < wait 數(shù)值,先前調(diào)用會被 clearTimeout ,也就不執(zhí)行;最終只執(zhí)行 1 次調(diào)用(即第 2 次的調(diào)用)
如果調(diào)用兩次間隔 > wait 數(shù)值,當(dāng)執(zhí)行 clearTimeout 的時候,前一次調(diào)用已經(jīng)執(zhí)行了;所以最終這兩次調(diào)用都會執(zhí)行
上述的實現(xiàn),是最經(jīng)典的 trailing 情況,即以 wait 間隔結(jié)束點(diǎn)作為函數(shù)調(diào)用計時點(diǎn),是我們平時用的最多的場景
B. 簡易 debounce - 只實現(xiàn) leading 功能另外用得比較多的就是以 wait 間隔開始點(diǎn)作為函數(shù)調(diào)用計時點(diǎn),即 leading 功能。
將上面代碼中最后的 setTimeout 內(nèi)容改成 timerId = undefined ,而將 fn.apply 提取出來加個 if 條件語句就行 ,修改后代碼如下:
//防抖代碼最簡單的實現(xiàn) function debounce(func, wait) { let timerId, result; return function() { if(timerId){ clearTimeout(timerId); // 每次觸發(fā) 都清除當(dāng)前timer,重新設(shè)置時間 } if(!timerId){ result = fn.apply(this, arguments); } timerId = setTimeout(function() { timerId = undefined; }, wait); return result; } }
fn.apply(lastThis, lastArgs) 之所以用 if 條件包裹,是針對首次調(diào)用的邊界情況
debounce 仍舊返回閉包(匿名函數(shù))
timerId 是閉包變量,相當(dāng)于標(biāo)志位,通過它可以知道某個函數(shù)的調(diào)用是否在上一次函數(shù)調(diào)用的影響范圍內(nèi)
假如調(diào)用該閉包兩次:
如果調(diào)用兩次間隔 < wait 數(shù)值,后調(diào)用因為仍在前一次的 wait 影響范圍內(nèi),所以會被 clearTimeout 掉;最終只執(zhí)行 1 次調(diào)用(即第 1 次的調(diào)用)
如果調(diào)用兩次間隔 > wait 數(shù)值,當(dāng)執(zhí)行第二次時 timerId 已經(jīng)是 underfined 的,所以會立即執(zhí)行 函數(shù),所以最終這兩次調(diào)用都會執(zhí)行
C. 簡易 throttle 函數(shù)throttle 函數(shù)的概念:函數(shù)在 n 秒內(nèi)只執(zhí)行一次,若這 n 秒內(nèi)還在有函數(shù)調(diào)用的請求都直接被忽略掉。
實現(xiàn)原理也很簡單:定義開關(guān)變量 canRun,在定時開啟的這段時間內(nèi)控制這個開關(guān)變量為canRun = false(上鎖),執(zhí)行完后才讓 canRun = true 即可。
function throttle(func, wait) { let canRun = true return function () { if (!canRun) { return // 如果開關(guān)關(guān)閉了,那就直接不執(zhí)行下邊的代碼 } canRun = false // 持續(xù)觸發(fā)的話,run一直是false,就會停在上邊的判斷那里 setTimeout(() => { func.apply(this, arguments) canRun = true // 定時器到時間之后,會把開關(guān)打開,我們的函數(shù)就會被執(zhí)行 }, wait) } }參考文章
Debouncing and Throttling Explained Through Examples:首推這篇經(jīng)典的文章,本文詳細(xì)描述了 lodash 中的 debounce 和 throttle 的思路設(shè)計;里面使用 圖文混排 深入淺出探討這兩函數(shù)的用法和具體使用場景,更為難得還嵌入有可交互 demo,能即刻感受這兩方法的具體使用方式;嫌看英文麻煩的可以看中文版 《實例解析防抖動(Debouncing)和節(jié)流閥(Throttling)》
防抖(debounce)函數(shù)的作用是什么?有哪些應(yīng)用場景,請實現(xiàn)一個防抖函數(shù):討論帖子,里面有不少的相關(guān)信息和資源
淺談 Underscore.js 中 _.throttle 和 _.debounce 的差異:很不錯的釋義文章,電梯類比秒懂
lodash.debounce: lodash debounce 多帶帶的庫,附官方文檔
防抖(debounce)函數(shù)的作用是什么:解釋了 debounce 函數(shù)的原理和實現(xiàn)
聊聊lodash的debounce實現(xiàn):作者對比了自己的實現(xiàn)和 lodash 中的實現(xiàn)
Confused about the maxWait option for LoDash’s debounce method:解釋 ‘maxWait’ 的作用
第 3 題:什么是防抖和節(jié)流?有什么區(qū)別?如何實現(xiàn):面試題,簡單快速實現(xiàn)防抖和節(jié)流這兩個函數(shù)
函數(shù)的防抖和節(jié)流是個啥???:用通俗的例子講解這兩個概念和實現(xiàn)
從lodash源碼學(xué)習(xí)節(jié)流與防抖:詳細(xì)注釋 lodash 中的 debounce 函數(shù)的實現(xiàn)
下面的是我的公眾號二維碼圖片,歡迎關(guān)注交流。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/105977.html
摘要:背景需要包寫起來爽,然而如果遇到?jīng)]有現(xiàn)成的化的工具函數(shù),就需要自己想辦法弄出一份類型聲明文件了。最為重要的是,這種遷移方面我們可以隨意自定義化中所需要的工具函數(shù),遷移粒度都可以由自己控制。 1、背景 1.1、需要 TS 包 TypeScript 寫起來爽,然而如果遇到?jīng)]有現(xiàn)成的 TS 化的工具函數(shù),就需要自己想辦法弄出一份類型聲明文件了。 前兩天要寫的小工具庫(Typescript 語...
摘要:如果我們的回調(diào)函數(shù)較為復(fù)雜,頁面的性能就會變差。而可以保證穩(wěn)定的時間間隔執(zhí)行一次回調(diào)函數(shù)。但需要弄清楚的是,無論是還是,控制的都是回調(diào)函數(shù)的執(zhí)行,而不是事件的監(jiān)聽。 前言 假設(shè)現(xiàn)在有個需求:監(jiān)聽滑動事件,并執(zhí)行回調(diào)。當(dāng)你用觸摸板或者鼠標(biāo)滑動頁面時,每秒鐘大概會觸發(fā)幾十次scroll事件,而當(dāng)你在手機(jī)等移動終端上滑動頁面時,每秒就會觸發(fā)一百次scroll事件。如果我們的回調(diào)函數(shù)較為復(fù)雜,...
摘要:事情是如何發(fā)生的最近干了件事情,發(fā)現(xiàn)了源碼的一個。樓主找到的關(guān)于和區(qū)別的資料如下關(guān)于拿來主義為什么這么多文章里會出現(xiàn)澤卡斯的錯誤代碼樓主想到了一個詞,叫做拿來主義。的文章,就深刻抨擊了拿來主義這一現(xiàn)象。 事情是如何發(fā)生的 最近干了件事情,發(fā)現(xiàn)了 underscore 源碼的一個 bug。這件事本身并沒有什么可說的,但是過程值得我們深思,記錄如下,各位看官仁者見仁智者見智。 平時有瀏覽別...
摘要:舉例舉例通過拖拽瀏覽器窗口,可以觸發(fā)很多次事件。不支持,所以不能在服務(wù)端用于文件系統(tǒng)事件??偨Y(jié)將一系列迅速觸發(fā)的事件例如敲擊鍵盤合并成一個單獨(dú)的事件。確保一個持續(xù)的操作流以每毫秒執(zhí)行一次的速度執(zhí)行。 Debounce 和 Throttle 是兩個很相似但是又不同的技術(shù),都可以控制一個函數(shù)在一段時間內(nèi)執(zhí)行的次數(shù)。 當(dāng)我們在操作 DOM 事件的時候,為函數(shù)添加 debounce 或者 th...
閱讀 1846·2021-09-14 18:03
閱讀 2278·2019-08-30 15:48
閱讀 1135·2019-08-30 14:09
閱讀 518·2019-08-30 12:55
閱讀 2740·2019-08-29 11:29
閱讀 1501·2019-08-26 13:43
閱讀 2324·2019-08-26 13:30
閱讀 2382·2019-08-26 12:17