lodash工具庫里有實現(xiàn)防抖和節(jié)流的函數(shù)
- 防抖和節(jié)流嚴格算起來應該屬于性能優(yōu)化的知識,但實際上遇到的頻率相當高,處理不當或者放任不管就容易引起瀏覽器卡死。所以還是很有必要早點掌握的。
-
先說一個常見的功能,很多網(wǎng)站會提供這么一個按鈕:用于返回頂部。
image.png - 這個按鈕只會在滾動到距離頂部一定位置之后才出現(xiàn),那么我們現(xiàn)在抽象出這個功能需求-- 監(jiān)聽瀏覽器滾動事件,返回當前滾條與頂部的距離
- 這個需求很簡單,直接寫:
function showTop () {
var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
console.log('滾動條位置:' + scrollTop);
}
window.onscroll = showTop
-
但是,在運行的時候會發(fā)現(xiàn)存在一個問題:這個函數(shù)的默認執(zhí)行頻率,太!高!了!。 高到什么程度呢?以chrome為例,我們可以點擊選中一個頁面的滾動條,然后點擊一次鍵盤的【向下方向鍵】,會發(fā)現(xiàn)函數(shù)執(zhí)行了8-9次!
image.png - 然而實際上我們并不需要如此高頻的反饋,畢竟瀏覽器的性能是有限的,不應該浪費在這里,所以接著討論如何優(yōu)化這種場景。
防抖和節(jié)流需要考慮作用域(apply)和參數(shù)傳遞(arguments)問題
防抖(debounce)
- 基于上述場景,首先提出第一種思路:在第一次觸發(fā)事件時,不立即執(zhí)行函數(shù),而是給出一個期限值比如200ms,然后:
- 如果在200ms內(nèi)沒有再次觸發(fā)滾動事件,那么就執(zhí)行函數(shù)
- 如果在200ms內(nèi)再次觸發(fā)滾動事件,那么當前的計時取消,重新開始計時
- 效果:如果短時間內(nèi)大量觸發(fā)同一事件,只會執(zhí)行一次函數(shù)。
- 實現(xiàn):既然前面都提到了計時,那實現(xiàn)的關(guān)鍵就在于setTimeOut這個函數(shù),由于還需要一個變量來保存計時,考慮維護全局純凈,可以借助閉包來實現(xiàn):
function debounce(func, wait) {
let timer = null
return function () {
let context = this
let args = arguments
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(context, args)
}, wait)
}
}
// 然后是舊代碼
function showTop () {
var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
console.log('滾動條位置:' + scrollTop);
}
window.onscroll = debounce(showTop,1000) // 為了方便觀察效果我們?nèi)€大點的間斷值,實際使用根據(jù)需要來配置
- 此時會發(fā)現(xiàn),必須在停止?jié)L動1秒以后,才會打印出滾動條位置。
- 到這里,已經(jīng)把防抖實現(xiàn)了,現(xiàn)在給出定義:
對于短時間內(nèi)連續(xù)觸發(fā)的事件(上面的滾動事件),防抖的含義就是讓某個時間期限(如上面的1000毫秒)內(nèi),事件處理函數(shù)只執(zhí)行一次。
節(jié)流(throttle)
- 繼續(xù)思考,使用上面的防抖方案來處理問題的結(jié)果是:
- 如果在限定時間段內(nèi),不斷觸發(fā)滾動事件(比如某個用戶閑著無聊,按住滾動不斷的拖來拖去),只要不停止觸發(fā),理論上就永遠不會輸出當前距離頂部的距離。
- 但是如果產(chǎn)品同學的期望處理方案是:即使用戶不斷拖動滾動條,也能在某個時間間隔之后給出反饋呢?
- 其實很簡單:我們可以設(shè)計一種類似控制閥門一樣定期開放的函數(shù),也就是讓函數(shù)執(zhí)行一次后,在某個時間段內(nèi)暫時失效,過了這段時間后再重新激活(類似于技能冷卻時間)。
- 效果:如果短時間內(nèi)大量觸發(fā)同一事件,那么在函數(shù)執(zhí)行一次之后,該函數(shù)在指定的時間期限內(nèi)不再工作,直至過了這段時間才重新生效。
- 實現(xiàn):這里借助setTimeout來做一個簡單的實現(xiàn),加上一個狀態(tài)位valid來表示當前函數(shù)是否處于工作狀態(tài):
// 完全不借助setTimeout,可以把狀態(tài)位換成時間戳,然后利用時間戳差值是否大于指定間隔時間來做判定
function throttle(func, wait) {
let prev = 0;
return function () {
let now = Date.now();
let context = this;
let args = arguments
if (now - prev > wait) {
func.apply(context, args);
prev = now;
}
}
}
// 將setTimeout的返回的標記當做判斷條件-判斷當前定時器是否存在,如果存在表示還在冷卻,并且在執(zhí)行fn之后消除定時器表示激活
function throttle(func, wait) {
let timer = null
return function () {
let context = this
let args = arguments
if (!timer) {
timer = setTimeout(() => {
timer = null
func.apply(context, args)
}, wait)
}
}
}
// 以下照舊
function showTop () {
var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
console.log('滾動條位置:' + scrollTop);
}
window.onscroll = throttle(showTop,1000)
運行以上代碼的結(jié)果是:如果一直拖著滾動條進行滾動,那么會以1s的時間間隔,持續(xù)輸出當前位置和頂部的距離
其他應用場景舉例
- 講完了這兩個技巧,下面介紹一下平時開發(fā)中常遇到的場景:
- 搜索框input事件,例如要支持輸入實時搜索可以使用節(jié)流方案(間隔一段時間就必須查詢相關(guān)內(nèi)容),或者實現(xiàn)輸入間隔大于某個值(如500ms),就當做用戶輸入完成,然后開始搜索,具體使用哪種方案要看業(yè)務需求。
- 頁面resize事件,常見于需要做頁面適配的時候。需要根據(jù)最終呈現(xiàn)的頁面情況進行dom渲染(這種情形一般是使用防抖,因為只需要判斷最后一次的變化情況)

