兩個鬧鐘,10 分鐘教你寫出 lodash 中的 debounce & throttle
- 2019 年 10 月 10 日
- 筆記
序
相比網上教程中的 debounce
函數,lodash 中的 debounce
功能更為強大,相應的理解起來更為複雜;
解讀源碼一般都是直接拿官方源碼來解讀,不過這次我們採用另外的方式:從最簡單的場景開始寫程式碼,然後慢慢往源碼上來靠攏,循序漸進來實現 lodash 中的 debounce
函數,從而更深刻理解官方 debounce 源碼的用意。
為了減少純程式碼帶來的晦澀感,本文以圖例來輔助講解,一方面這樣能減少源碼閱讀帶來的枯燥感,同時也讓後續回憶源碼內容更加的具體形象。(記住圖的內容,後續再寫出源碼也變得簡單些)
在本文的末尾還會附上簡易的 debounce & throttle
的實現的程式碼片段,方便平時快速用在簡單場景中,免去引用 lodash
庫。
本文屬於源碼解讀類型的文章,對 debounce 還不熟悉的讀者建議先通過參考文章(在文末)了解該函數的概念和用法。
1、用圖例解析 debounce 源碼
附源碼 debounce: https://github.com/boycgit/ts-debounce-throttle/blob/master/src/lib/debounce.ts#L80
首先搬出 debounce
(防抖)函數的概念:函數在 wait
秒內只執行一次,若這 wait
秒內,函數高頻觸發,則會重新計算時間。
看似簡單一句話,內含乾坤。為方便行文敘述,約定如下術語:
- 假定我們要對
func
函數進行debounce
處理,經 debounced 後的返回值我們稱之為debounced func
wait
表示傳入防抖函數的時間time
表示當前時間戳lastCallTime
表示上一次調用debounced func
函數的時間lastInvokeTime
表示上一次func
函數執行的時間result
是每次調用debounced func
函數的返回值- 用
time
表示當前時間
本文將搭配圖例 + 程式程式碼的方式,將上述概念具象到圖中。
2、最簡單的案例
以最簡單的情景為例:在某一時刻點只調用一次 debounced func
函數,那麼將在 wait
時間後才會真正觸發 func
函數。
將這個情景形成一幅圖例,最終繪製出的圖如下所示:

簡單場景下的圖例
下面我們詳細講解這幅圖的產生過程,其實不難,基本上看一遍就懂。
首先繪製在圖中放置一個黑色鬧鐘表示用戶調用 debounced func
函數:(同時用 lastCallTime
標示出最近一次調用 debounced func
的時間)

繪製黑色鬧鐘表示調用 debounced func
同時在距離該黑色鬧鐘 wait
處放置一個藍色鬧鐘,表示setTimout(..., wait)
,該藍色鬧鐘表示未來當程式碼運行到該時間點時,需要做一些判斷:

放置一個藍色鬧鐘
為了標示出表示程式當前運行的進度(當前時間戳),我們用橙紅色滑塊來表示:

橙紅色表示當前時間戳
當紅色滑塊到達該藍色鬧鐘處的時候,藍色鬧鐘會進行判斷:因為當前滑塊距離最近的黑色鬧鐘的時間差為 wait
:

判斷時間差為 wait
故而做出判斷(依據 debounce
函數的功能定義):需要觸發一次 func
函數,我們用紅色鬧鐘來表示 func
函數的調用,所以就放置一個紅色鬧鐘

放置紅色鬧鐘,表示 func 函數被調用
很顯然藍色和紅色鬧鐘重疊起來的。
同時我們給紅色鬧鐘標上 lastInvokeTime
,記錄最近一次調用 func
的時間:

給紅色鬧鐘標上 lastInvokeTime
注意
lastInvokeTime
和lastCallTime
的區別,兩者含義是不一樣的
這樣我們就完成了最簡單場景下 debounce
圖例的繪製,簡單易懂。
後續我們會逐漸增加黑色鬧鐘出現的複雜度,不斷去分析紅色鬧鐘的位置。這樣就能將理解 debounce
源碼的問題轉換成「根據圖上黑色鬧鐘的位置,請畫出紅色鬧鐘位置」的問題,而分析紅色鬧鐘位置的過程中也就是理解 debounce
源碼的過程;
用圖例方式輔助理解源碼的方式可以減少源碼閱讀帶來的枯燥感,同時後續回憶源碼內容起來也更加具體形象。
為避免後續寫文章到處解釋圖中元素的概念含義,這裡不妨先羅列出來,如果閱讀過程中忘記到這裡回憶一下也會方便許多:
- 橫線代表時間軸,橙紅色滑塊代表當前時間
time
- 每個黑色箭頭表示
debounced func
函數的調用 - 黑色鬧鐘表示調用
debounced func
函數時的時間,最後一次黑色鬧鐘上標上lastCallTime
,表示最近一次調用的時間戳; - 紅色鬧鐘表示調用
func
函數的時間,最後一次紅色鬧鐘上標上lastInvokeTime
,表示最近一次調用的時間戳; - 此外還有一個藍色鬧鐘,表示 setTimeout 時間戳(用來規劃
func
函數執行的時間),每次時間軸上的橙紅色滑塊到這個時間點就要做判斷:是執行func
或者推遲藍色鬧鐘位置
有關藍色鬧鐘,這裡有兩個注意點:
- 時間軸上最多同時只有一個藍色鬧鐘;
- 只有在第一次調用
debounced func
函數時才會在wait
時間後放置藍色鬧鐘,後續鬧鐘的出現位置就由藍色鬧鐘自己決策(下文會舉例說明)
3、有 N 多個黑色鬧鐘的場景
現在我們來一個稍微複雜的場景:
假如在 wait
時間內(記住這個前提條件)調用 n 次 debounced func
函數,如下所示:

調用 n 次debounced func
函數
第一次調用 debounced func
函數會在 wait
時間後放置藍色鬧鐘(只有第一次調用會放置藍色鬧鐘,後續鬧鐘的位置由藍色鬧鐘自己決策):

放置藍色鬧鐘
以上就是描述,那麼問題來了:請問紅色鬧鐘應該出現在時間軸哪個位置?
3.1、分析紅色鬧鐘出現的位置
我們只關注最後一個黑色鬧鐘,並假設藍色鬧鐘距離該黑色鬧鐘時間間隔為 x
:

假設兩鬧鐘距離 x
那麼第一個黑色鬧鐘和最後一個黑色鬧鐘的時間間隔是 wait - x
:

兩個黑鬧鐘間距
接下來我們關注橙紅色滑塊(即當前時間time
)到達藍色鬧鐘的時,藍色鬧鐘開始做決策:計算可知 x < wait
,此時藍色鬧鐘決定不放置紅色鬧鐘(即不觸發 func
),而是將藍色鬧鐘往後挪了挪,挪動距離為 wait - x
,調整完之後的藍色鬧鐘位置如下:

調整後藍色鬧鐘位置
之所以挪 wait - x
的距離,是因為挪完後的藍色鬧鐘距離最後一個黑色鬧鐘恰好為 wait
間隔(從而保證 debounce
函數至少間隔 wait
時間 才觸發的條件):

保證挪完後的藍色鬧鐘距離最後一個黑色鬧鐘恰好為 wait
間隔
從挪移之後開始,到下一次橙色鬧鐘再次遇到藍色鬧鐘這段期間,我們暫且稱之為 」藍色決策間隔期「(請忍耐這抽象的名稱,畢竟我想了好久),藍色鬧鐘基於此間隔期的內容來進行決策,只有兩種決策:
- 如果在」藍色決策間隔期「內沒有黑鬧鐘出現,那麼紅色滑塊達到藍色鬧鐘的時候,藍色鬧鐘計算獲知當前藍色鬧鐘距離上一個黑色鬧鐘的時間間隔不少於
wait
(time - lastCallTime >= wait
),那就會放置紅色鬧鐘(即調用func
),目標達成;

」藍色決策間隔期「內沒有黑鬧鐘出現,則可以直接放置紅色鬧鐘
- 如果在」藍色決策間隔期「內仍舊有黑鬧鐘出現,那麼當橙紅色滑塊到達藍色鬧鐘時,藍色鬧鐘又會重新計算與該間隔期內最後一隻黑色鬧鐘的距離
y
,隨後 又會往後挪動位置 `wait-y`,再一次保證藍色鬧鐘距離最後一個黑色鬧鐘恰好為wait
間隔 —— 沒錯,又形成了新的 」藍色決策間隔期「;那接下去的分析就又回到了 這裡兩點(即遞歸決策),直到能放置到紅鬧鐘為止。

重新形成」藍色決策間隔期「
從上我們可以看到,藍色鬧鐘一直保持 」紳士「 風範,隨著黑色鬧鐘的逼近,藍色鬧鐘一直保持」克制「態度,不斷調整自己的位置,讓調整後的位置總是和最後一個黑色鬧鐘保持 wait
的距離。
3.2、用程式碼描述圖例過程
我們用程式碼將上述的過程描述出來,就是下面這個樣子:
function debounce(func, wait, options) { var lastArgs, lastThis, result, timerId, lastCallTime, lastInvokeTime = 0, trailing = true; wait = toNumber(wait) || 0; // 紅色滑塊達到藍色鬧鐘時,藍色鬧鐘根據條件作出決策 function timerExpired() { var time = now(); // 決策 1: 滿足放置紅色鬧鐘的條件,則放置紅鬧鐘 if (shouldInvoke(time)) { return trailingEdge(time); } // 否則,決策 2:將藍色鬧鐘再往後挪 `wait-x` 位置,形成 」藍色決策間隔期「 timerId = setTimeout(timerExpired, remainingWait(time)); } // === 以下是具體決策中的函數實現 ==== // 做出 」應當放置紅色鬧鐘「 的決策的條件:藍色鬧鐘和最後一個黑色鬧鐘的間隔不小於 wait 間隔 function shouldInvoke(time) { var timeSinceLastCall = time - lastCallTime; return ( timeSinceLastCall >= wait ); } // 具體函數:放置紅色鬧鐘 function trailingEdge(time) { timerId = undefined; if (trailing && lastArgs) { return invokeFunc(time); } lastArgs = lastThis = undefined; return result; } // 具體函數 - 子函數:在時間軸上放置紅鬧鐘 function invokeFunc(time) { var args = lastArgs, thisArg = lastThis; lastArgs = lastThis = undefined; lastInvokeTime = time; result = func.apply(thisArg, args); return result; } // 具體函數:計算讓藍色鬧鐘往後挪 wait-x 位置 function remainingWait(time) { var timeSinceLastCall = time - lastCallTime, timeWaiting = wait - timeSinceLastCall; return timeWaiting ; } // ============== // 主流程:讓紅色滑塊在時間軸上前進(即 debounced func 函數的執行) function debounced() { var time = now(); lastArgs = arguments; lastThis = this; lastCallTime = time; if (timerId === undefined) { timerId = setTimeout(timerExpired, wait); } return result; } return debounced; }
這部分程式碼還請不要略過,因為程式碼是從debounce
源碼中整理過來,除了函數順序略有調整外,源碼風格保持原有的,相當於直接閱讀源碼。每個函數都有注釋,對比著圖例閱讀下來相信讀完會有收穫的。
上述這份程式碼已經包含了 debounce
源碼的核心骨架,接下來我們繼續擴展場景,將源碼內容豐滿起來。
4、豐富功能特性
4.1、支援 leading 特性
leading
功能簡單理解就是,在第一次(注意這個條件)放下黑色鬧鐘的時候:
- 立即放置紅鬧鐘,同時在
- 此後
wait
處放置方式藍色鬧鐘(註:第一次放下黑色鬧鐘的時候,按理說也會在wait
處放下藍色鬧鐘,考慮既然leading
也有這種操作,那麼就不多此一舉。記住:整個時間軸上最多只能同時有一個藍色鬧鐘)
用圖說話:

支援 leading 功能
第一次放置黑色鬧鐘的時候,會疊加上紅色鬧鐘(當然這個紅色鬧鐘上會標示 lastInvokeTime
),另外在 wait
間隔後會有藍色鬧鐘。其他流程和之前案例分析一樣。
在程式碼層面,我們給剛才的 debounce
函數添加 leading
功能(通過 options.leading
開啟)、新增一個 leadingEdge
方法後,再微調剛才的程式碼:
function debounce(func, wait, options) { ... var leading = false; // 默認不開啟 leading = !!options.leading; // 通過 options.leading 開啟 ... // 首先:新增執行 leading 處的操作的函數 function leadingEdge(time) { lastInvokeTime = time; // 設置 lastInvokeTime 時間標籤 timerId = setTimeout(timerExpired, wait); // 同時在此後 `wait` 處放置一個藍色鬧鐘 return leading ? invokeFunc(time) : result; // 如果開啟,直接放置紅色鬧鐘;否則直接返回 result 數值 } ... // 其次:給放置紅色鬧鐘新增一種條件 function shouldInvoke(time) { ... return ( lastCallTime === undefined || // 初次執行時 timeSinceLastCall >= wait // 或者前面分析的條件,兩次 `debounced func` 調用間隔大於 wait ); } // 注意:放置完紅色鬧鐘後,記得要清空 timerId,相當於清空時間軸上藍色鬧鐘; function trailingEdge(time) { timerId = undefined; ... } // 最後:leading 邊界調用 function debounced(){ ... var isInvoking = shouldInvoke(time); // 判斷是否可以放置紅色鬧鐘 ... if (isInvoking) { // 如果可以放置紅色鬧鐘 if (timerId === undefined) { // 且當時間軸上沒有藍色鬧鐘 // 執行 leading 邊界處操作(放置紅色鬧鐘 或 直接返 result) return leadingEdge(lastCallTime); } } ... return result; } return debounced; }
4.2、支援 maxWait 特性
要理解這個 maxWait
特性,我們先看一種特殊情況,在 {leading: false} 下, 時間軸上我們很密集地放置黑色鬧鐘:
按之前的所述規則,我們的藍色鬧鐘一直保持紳士態度,隨著黑色鬧鐘的逼近,藍色鬧鐘將不斷將調整自己的位置,讓自己調整後的位置總是和最後一個黑色鬧鐘保持 wait
的距離:

密集的黑色鬧鐘將會讓藍色鬧鐘無處安放
那麼在這種情況下,如果黑色鬧鐘一直保持這種密集放置狀態,理論上就紅色鬧鐘就沒有機會出現在時間軸上。
那在這種情況下能否實現一個功能,無論黑色鬧鐘多麼密集,時間軸上最多隔 maxWait
時間就出現紅色鬧鐘,就像下圖那樣:

使用 maxWait 保證紅色鬧鐘能出現
有了這個功能屬性後,藍色鬧鐘從此 」變得堅強「,也有了 "底線",縱使黑色鬧鐘的不斷逼近,也會堅守 maxWait
底線,到點就放置紅色鬧鐘。
實現該特性的大致思路如下:
maxWait
是與lastInvokeTime
共同協作- 在藍色鬧鐘計算後退距離時,
maxWait
發揮作用;在沒有maxWait
的時候,是按上一次黑色鬧鐘進行測距,保證調整後的藍色鬧鐘和黑色鬧鐘保持wait
的距離;而在有了maxWait
後,藍色鬧鐘調整距離還會考慮上一次紅色鬧鐘的位置,保持調整後鬧鐘的位置和紅色鬧鐘距離不能超過 `maxWait`,這就是底線了,到了一定程度,就算黑色鬧鐘在逼近,藍色鬧鐘也不會 」退縮「:

受到 maxWait 影響,藍色鬧鐘的位置有了 」底線「
從程式碼層面上看, maxWait
具體是在 remainingWait
方法 和 shouldInvoke
中發揮作用的:
function debounce(func, wait, options) { ... var lastInvokeTime = 0; // 初始化 var maxing = false; // 默認沒有底線 maxing = 'maxWait' in options; maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait; // 從 options.maxWait 中獲取底線數值 ... // 首先,在在藍色鬧鐘決策後退多少距離時,maxWait 發揮了作用 function remainingWait(time) { var timeSinceLastCall = time - lastCallTime, timeSinceLastInvoke = time - lastInvokeTime, timeWaiting = wait - timeSinceLastCall; // 在這裡發揮作用,保持底線 return maxing ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting; } ... // 其次:針對 `maxWait`,給放置紅色鬧鐘新增一種可能條件 function shouldInvoke(time) { ... var timeSinceLastInvoke = time - lastInvokeTime; // 獲取距離上一次紅色鬧鐘的時間間隔 return ( lastCallTime === undefined || // 初次執行時 timeSinceLastCall >= wait || // 或者前面分析的條件,兩次 `debounced func` 調用間隔大於 wait (maxing && timeSinceLastInvoke >= maxWait) // 兩次紅色鬧鐘間隔超過 maxWait ); } // 最後:leading 邊界調用 function debounced(){ ... var isInvoking = shouldInvoke(time); // 判斷是否可以放置紅色鬧鐘的條件 ... if (isInvoking) { // 如果可以放置紅色鬧鐘 ... // 邊界情況的處理,保證在緊 loop 中能正常保持觸發 if (maxing) { timerId = setTimeout(timerExpired, wait); return invokeFunc(lastCallTime); } } ... return result; } return debounced; }
因此,maxWait
能夠讓紅色鬧鐘保證在 maxWait
間隔內至少出現 1 次;
4.3、支援 cancel / flush 方法
這兩個函數是為了能隨時控制 debounce
的快取狀態;
其中 cancel
方法源碼如下:
// 取消防抖 function cancel() { if (timerId !== undefined) { clearTimeout(timerId); } lastInvokeTime = 0; lastArgs = lastCallTime = lastThis = timerId = undefined; }
調用該方法,相當於直接在時間軸上去除藍色鬧鐘,這樣紅色方塊(時間)就永遠遇見不了藍色鬧鐘,那樣也就不會有放置紅色鬧鐘的可能了。
其中 flush
方法源碼如下:
function flush() { return timerId === undefined ? result : trailingEdge(now()); }
非常直觀,調用該方法相當於直接在時間軸上放置紅色鬧鐘。
至此,我們已經完整實現了 lodash 的 debounce
函數,也就相當於閱讀了一遍其源碼。
5、實現 throttle 函數
在完成上面 debounce
功能和特性後(尤其是 maxWait
特性),就能藉助 debounce
實現 throttle
函數了。
看 throttle 源碼 就能明白:
function throttle(func, wait, options) { var leading = true, trailing = true; // ... return debounce(func, wait, { 'leading': leading, 'maxWait': wait, 'trailing': trailing }); }
所以在 lodash
中,只需要 debounce
函數即可,throttle
相當於 」充話費「 送的。
至此,我們已經解讀完 lodash 中的 debounce & throttle
函數源碼;
最後附帶一張 lodash 實現執行效果圖,用來自測是否真的理解通透:

loadash 執行效果圖
註:此圖取自於文章《 聊聊lodash的debounce實現》
6、小結
在前端領域的性能優化手段中,防抖(debounce
)和節流(throttle
)是必備的技能,網上隨便一搜就有很多文章去分析解釋,不乏優秀的文章使用 圖文混排 + 類比方式 深入淺出探討這兩函數的用法和使用場景(見文末的參考文檔)。
那我為什麼還要寫這一篇文章?
緣起前兩天手動將 lodash 中的 debounce
和 throttle
兩個函數 TS 化的需求,而平時我也只是使用並沒有在意它們真正的實現原理,因此在遷移過程我順帶閱讀了一番 lodash 中這兩個函數的源碼。
具體原因和遷移過程請移步《技巧 – 快速 TypeScript 化 lodash 中的 throttle & debounce 函數》
本文嘗試提供了另一個視角去解讀,通過時間軸 + 鬧鐘圖例 + 程式碼的方式來解讀 lodash 中的 debounce
& throttle
源碼; 整個流程下來只要理解了黑色、藍色、紅色這 3 種鬧鐘的關係,那麼憑著理解力去實現簡版 lodash 的 debounce
函數並非難事。
當然上述的敘述中,略過了很多細節和存在性的判斷(諸如 timeId
的存在性判斷、isInvoking
的出現位置等),省略這主要是為了降低源碼閱讀的難度;(實際中這些細節的處理有時候反而很重要,是程式碼健壯性不可或缺的一部分)
希望本文能對讀者理解 lodash 中的 debounce
& throttle
源碼有些許的幫助,歡迎隨時關注微信公眾號或者技術部落格留言交流。
【附】程式碼片段
如果在你僅僅需要應付簡單的一些場景,也可以直接使用下方的程式碼片段。
A. 簡易 debounce – 只實現 `trailing` 情況
防抖函數的概念:函數在 n
秒內只執行一次,若這 n
秒內,函數高頻觸發,則會重新計算時間。
將這段話翻譯成程式碼,你會發現並不難:
//防抖程式碼最簡單的實現 function debounce(func, wait) { let timerId, result; return function() { if(timerId){ clearTimeout(timerId); // 每次觸發 都清除當前timer,重新設置時間 } timerId = setTimeout(function(){ result = func.apply(this, arguments); }, wait); return result; } }
- debounce 返回閉包(匿名函數)
- 假如調用該閉包兩次:
如果調用兩次間隔 < wait 數值,先前調用會被 clearTimeout ,也就不執行;最終只執行 1 次調用(即第 2 次的調用)
如果調用兩次間隔 > wait 數值,當執行 clearTimeout 的時候,前一次調用已經執行了;所以最終這兩次調用都會執行
上述的實現,是最經典的 trailing
情況,即以 wait 間隔結束點作為函數調用計時點,是我們平時用的最多的場景
B. 簡易 debounce – 只實現 `leading` 功能
另外用得比較多的就是以 wait 間隔開始點作為函數調用計時點,即 leading
功能。
將上面程式碼中最後的 setTimeout
內容改成 timerId = undefined
,而將 fn.apply
提取出來加個 if
條件語句就行 ,修改後程式碼如下:
//防抖程式碼最簡單的實現 function debounce(func, wait) { let timerId, result; return function() { if(timerId){ clearTimeout(timerId); // 每次觸發 都清除當前timer,重新設置時間 } if(!timerId){ result = fn.apply(this, arguments); } timerId = setTimeout(function() { timerId = undefined; }, wait); return result; } }
fn.apply(lastThis, lastArgs)
之所以用 if 條件包裹,是針對首次調用的邊界情況
- debounce 仍舊返回閉包(匿名函數)
timerId
是閉包變數,相當於標誌位,通過它可以知道某個函數的調用是否在上一次函數調用的影響範圍內- 假如調用該閉包兩次:
- 如果調用兩次間隔 < wait 數值,後調用因為仍在前一次的 wait 影響範圍內,所以會被 clearTimeout 掉;最終只執行 1 次調用(即第 1 次的調用)
- 如果調用兩次間隔 > wait 數值,當執行第二次時 `timerId` 已經是 `underfined` 的,所以會立即執行 函數,所以最終這兩次調用都會執行
C. 簡易 throttle 函數
throttle
函數的概念:函數在 n
秒內只執行一次,若這 n
秒內還在有函數調用的請求都直接被忽略掉。
實現原理也很簡單:定義開關變數 canRun
,在定時開啟的這段時間內控制這個開關變數為canRun = false
(上鎖),執行完後才讓 canRun = true
即可。
function throttle(func, wait) { let canRun = true return function () { if (!canRun) { return // 如果開關關閉了,那就直接不執行下邊的程式碼 } canRun = false // 持續觸發的話,run一直是false,就會停在上邊的判斷那裡 setTimeout(() => { func.apply(this, arguments) canRun = true // 定時器到時間之後,會把開關打開,我們的函數就會被執行 }, wait) } }
REFERENCE
參考文檔
- Debouncing and Throttling Explained Through Examples:首推這篇經典的文章,本文詳細描述了 lodash 中的 debounce 和 throttle 的思路設計;裡面使用 圖文混排 深入淺出探討這兩函數的用法和具體使用場景,更為難得還嵌入有可交互 demo,能即刻感受這兩方法的具體使用方式;嫌看英文麻煩的可以看中文版 《實例解析防抖動(Debouncing)和節流閥(Throttling)》
- 防抖(debounce)函數的作用是什麼?有哪些應用場景,請實現一個防抖函數:討論帖子,裡面有不少的相關資訊和資源
- 淺談 Underscore.js 中 .throttle 和 .debounce 的差異:很不錯的釋義文章,電梯類比秒懂
- lodash.debounce: lodash debounce 單獨的庫,附官方文檔
- 防抖(debounce)函數的作用是什麼:解釋了 debounce 函數的原理和實現
- 聊聊lodash的debounce實現:作者對比了自己的實現和 lodash 中的實現
- Confused about the maxWait option for LoDash』s debounce method:解釋 『maxWait』 的作用
- 第 3 題:什麼是防抖和節流?有什麼區別?如何實現:面試題,簡單快速實現防抖和節流這兩個函數
- 函數的防抖和節流是個啥???:用通俗的例子講解這兩個概念和實現
- 從lodash源碼學習節流與防抖:詳細注釋 lodash 中的 debounce 函數的實現
—END—