防抖和節(jié)流嚴格算起來應該屬于性能優(yōu)化的知識,但實際上遇到的頻率相當高,處理不當或者放任不管就容易引起瀏覽器卡死。所以還是很有必要早點掌握的致燥。(信我,你看完肯定就懂了)
從滾動條監(jiān)聽的例子說起
先說一個常見的功能怖侦,很多網站會提供這么一個按鈕:用于返回頂部篡悟。
這個按鈕只會在滾動到距離頂部一定位置之后才出現(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次杨幼!然而實際上我們并不需要如此高頻的反饋撇簿,畢竟瀏覽器的性能是有限的,不應該浪費在這里差购,所以接著討論如何優(yōu)化這種場景四瘫。
防抖(debounce)觸發(fā)高頻事件后 n 秒內函數(shù)只會執(zhí)行一次,如果 n 秒內高頻事件再次被觸發(fā)欲逃,則重新計算時間
基于上述場景找蜜,首先提出第一種思路:在第一次觸發(fā)事件時,不立即執(zhí)行函數(shù)稳析,而是給出一個期限值比如200ms洗做,然后:
如果在200ms內沒有再次觸發(fā)滾動事件,那么就執(zhí)行函數(shù)
如果在200ms內再次觸發(fā)滾動事件彰居,那么當前的計時取消竭望,重新開始計時
效果:如果短時間內大量觸發(fā)同一事件,只會在操作結束后一定時間內執(zhí)行一次函數(shù)裕菠。
實現(xiàn):既然前面都提到了計時,那實現(xiàn)的關鍵就在于setTimeout這個函數(shù)闭专,由于還需要一個變量來保存計時奴潘,考慮維護全局純凈旧烧,可以借助閉包來實現(xiàn):
/*
* fn [function] 需要防抖的函數(shù)
* delay [number] 毫秒,防抖期限值
*/
function debounce(fn,delay){
let timer = null //借助閉包
return function() {
if(timer){
clearTimeout(timer) //進入該分支語句画髓,說明當前正在一個計時過程中掘剪,并且又觸發(fā)了相同事件。所以要取消當前的計時奈虾,重新開始計時
timer = setTimeout(()=> {
fn()
},delay)
}else{
timer = setTimeout(()=> {
fn()
},delay) // 進入該分支說明當前并沒有在計時夺谁,那么就開始一個計時
}
}
}
當然 上述代碼是為了貼合思路,方便理解肉微,寫完會發(fā)現(xiàn)其實 time = setTimeout(fn,delay)是一定會執(zhí)行的匾鸥,所以可以稍微簡化下:
/*****************************簡化后的分割線 ******************************/
function debounce(fn,delay){
let timer = null //借助閉包
return function() {
if(timer){
clearTimeout(timer)
}
timer = setTimeout(()=> {
fn()
},delay) // 簡化寫法
}
}
// 然后是舊代碼
function showTop () {
var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
console.log('滾動條位置:' + scrollTop);
}
window.onscroll = debounce(showTop,1000) // 為了方便觀察效果我們取個大點的間斷值,實際使用根據需要來配置
此時會發(fā)現(xiàn)碉纳,必須在停止?jié)L動1秒以后勿负,才會打印出滾動條位置。
到這里劳曹,已經把防抖實現(xiàn)了
實現(xiàn)方式:每次觸發(fā)事件時設置一個延遲調用方法奴愉,并且取消之前的延時調用方法
缺點:如果事件在規(guī)定的時間間隔內被不斷的觸發(fā),則調用方法會被不斷的延遲
節(jié)流(throttle)高頻事件觸發(fā)铁孵,但在 n 秒內只會執(zhí)行一次锭硼,所以節(jié)流會稀釋函數(shù)的執(zhí)行頻率
繼續(xù)思考,使用上面的防抖方案來處理問題的結果是:
如果在限定時間段內蜕劝,不斷觸發(fā)滾動事件(比如某個用戶閑著無聊檀头,按住滾動不斷的拖來拖去),只要不停止觸發(fā)熙宇,理論上就永遠不會輸出當前距離頂部的距離鳖擒。
但是如果產品同學的期望處理方案是:即使用戶不斷拖動滾動條,也能在某個時間間隔之后給出反饋呢烫止?(此處暫且不論哪種方案更合適蒋荚,既然產品爸爸說話了我們就先考慮怎么實現(xiàn))
其實很簡單:我們可以設計一種類似控制閥門一樣定期開放的函數(shù),也就是讓函數(shù)執(zhí)行一次后馆蠕,在某個時間段內暫時失效期升,過了這段時間后再重新激活(類似于技能冷卻時間)。
效果:如果短時間內大量觸發(fā)同一事件互躬,那么在函數(shù)執(zhí)行一次之后播赁,該函數(shù)在指定的時間期限內不再工作,直至過了這段時間才重新生效吼渡。
實現(xiàn) 這里借助setTimeout來做一個簡單的實現(xiàn)容为,加上一個狀態(tài)位valid來表示當前函數(shù)是否處于工作狀態(tài):
function throttle(fn,delay){
let valid = true
return function() {
if(!valid){
//休息時間 暫不接客
return false
}
// 工作時間,執(zhí)行函數(shù)并且在間隔期內把狀態(tài)位設為無效
fn()
valid = false
setTimeout(() => {
valid = true;
}, delay)
}
}
/* 請注意,節(jié)流函數(shù)并不止上面這種實現(xiàn)方案,
例如可以完全不借助setTimeout坎背,可以把狀態(tài)位換成時間戳替劈,然后利用時間戳差值是否大于指定間隔時間來做判定。
也可以直接將setTimeout的返回的標記當做判斷條件-判斷當前定時器是否存在得滤,如果存在表示還在冷卻陨献,并且在執(zhí)行fn之后消除定時器表示激活,原理都一樣
*/
// 以下照舊
function showTop () {
var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
console.log('滾動條位置:' + scrollTop);
}
window.onscroll = throttle(showTop,1000)
運行以上代碼的結果是:
如果一直拖著滾動條進行滾動懂更,那么會以1s的時間間隔眨业,持續(xù)輸出當前位置和頂部的距離
實現(xiàn)方式:每次觸發(fā)事件時,如果當前有等待執(zhí)行的延時函數(shù)沮协,則直接return
區(qū)別:防抖動是將多次執(zhí)行變?yōu)樽詈笠淮螆?zhí)行龄捡,節(jié)流是將多次執(zhí)行變成每隔一段時間執(zhí)行。
其他應用場景舉例
講完了這兩個技巧皂股,下面介紹一下平時開發(fā)中常遇到的場景:
1.搜索框input事件墅茉,例如要支持輸入實時搜索可以使用節(jié)流方案(間隔一段時間就必須查詢相關內容),或者實現(xiàn)輸入間隔大于某個值(如500ms)呜呐,就當做用戶輸入完成就斤,然后開始搜索,具體使用哪種方案要看業(yè)務需求蘑辑。
2.頁面resize事件洋机,常見于需要做頁面適配的時候。需要根據最終呈現(xiàn)的頁面情況進行dom渲染(這種情形一般是使用防抖洋魂,因為只需要判斷最后一次的變化情況)
參考來源:https://segmentfault.com/a/1190000018428170