性能提速:debounce(防抖)、throttle(節流/限頻)
debounce與throttle是用戶交互處理中常用到的性能提速方案,debounce用來實現防抖動,throttle用來實現節流(限頻)。那麽這兩個方法到底是什麽(what)?為何要用(why-解決什麽問題)?具體的實現原理,以及函數運行過程是怎樣的呢(how)?
1、what?
連續操作:兩個操作之間的時間間隔小於設定的閥值,這樣子的一連串操作視為連續操作。
debounce(防抖):一個連續操作中的處理,只觸發一次,從而實現防抖動。
throttle:一個連續操作中的處理,按照閥值時間間隔進行觸發,從而實現節流。
圖1 debounce、throttle運行圖
如圖所示,其中delay=4,由於紅色操作序列與綠色操作序列之間的時間間隔小於delay
- debounceTail:執行操作在連續操作完成之後,觸發;
- debounceStart:執行操作在連續操作完成之前,觸發;
- throttle:在一個連續操作行為中,每間隔delay的時間觸發1次。
結合運行圖,可以更好的理解debounce、throttle的作用。
2、why?
常用情景:
- a、scroll事件:當頁面發生滾動時,scroll事件會被頻繁的觸發,1s觸發可高達上百次。在scroll事件中,如果有復雜的操作(特別是影響布局的操作),將會大大影響性能,甚至導致瀏覽器崩潰。所以,對其進行防抖、限頻很重要。
- b、click事件:用戶進行click事件時,有可能連續觸發點擊(用戶本意並非雙擊)。該操作有可能是不小心多次連續點擊,也可能是頁面狀況不好的情況下,期待盡快得到反饋的有意行為;但這樣的操作,反而會加劇性能問題,因此也有必要考慮防抖、限頻。
- c、input事件:如sug等需要通過ajax及時獲得數據的情況,需要進行限頻,防止頻繁的請求發生,減少服務器壓力的同時,提高頁面響應性能。
- d、touchmove事件:同scroll事件類似。
還有許多其他業務場景會出現頻繁操作的情況,不一一列舉。debounce可用於:防止用戶的多次click提交;scroll下拉刷新時,同一位置多次請求數據等。throttle可應用於,scroll設置定位等的頻繁位置計算;拖拽的頻繁位置計算等。
3、how?
怎樣實現?其代碼實現如下:
// 防抖 且首次執行 // 采用原理:第一操作觸發,連續操作時,最後一次操作打開任務開關(並非執行任務),任務將在下一次操作時觸發) function debounceStart(fn, delay, ctx) { let immediate = true let movement = null return function() { let args = arguments // 開關打開時,執行任務 if (immediate) { fn.apply(ctx, args) immediate = false } // 清空上一次操作 clearTimeout(movement) // 任務開關打開 movement = setTimeout(function() { immediate = true }, delay) } } // 防抖 尾部執行 // 采用原理:連續操作時,上次設置的setTimeout被clear掉 function debounceTail(fn, delay, ctx) { let movement = null return function() { let args = arguments // 清空上一次操作 clearTimeout(movement) // delay時間之後,任務執行 movement = setTimeout(function() { fn.apply(ctx, args) }, delay) } } // 限頻,每delay的時間執行一次 function throttle(fn, delay, ctx) { let isAvail = true return function() { let args = arguments // 開關打開時,執行任務 if (isAvail) { fn.apply(ctx, args) isAvail = false // delay時間之後,任務開關打開 setTimeout(function() { isAvail = true }, delay) } } } // 調用 btn.onclick = debounceStart(function(event) { console.log('100ms') }, 100, this) window.onscroll = throttle(function(event) { console.log('100ms') }, 100, this)
如上代碼,使用了閉包,將isAvail等父級變量存儲在了內存當中,實現狀態切換。同時,通過apply將任務函數的上下文ctx(在類、對象內操作時,其作用更明顯);參數arguments(如調用中的event),存入最終的任務執行函數當中。通過timer的clear和set來控制任務的觸發,同時需留意任務執行與任務開關打開的區別。任務執行是timer到達,就將觸發任務;任務開關打開是timer到達時,只將狀態變更,需要用戶的再一次操作,才能實施真正的任務觸發。
通過控制臺可以看到,不進行限頻時,scroll在1s內可以觸發高達上100次,增加了限頻之後,就將scroll的觸發控制在一定的範圍內。
4、思考
圖2 throttle運行標示圖
在實際的使用場景當中,我們會發現,用戶最後一次操作並沒有後續的處理,也就是最後一次操作的狀態將丟失。在某些應用場景當中,可能造成狀態處理不準確。如通過scroll事件判斷是否到達頁面底部,如果到達,則提示用戶。使用throttle方法進行節流,在到達底部之前,小於delay的時間間隔內,觸發了一次位置判斷操作;下一次觸發將在delay時間之後,但在那之前,scroll事件已經結束了,所以無法獲取最後scroll到底部的位置,也就不會觸發提示。
如何優化呢?
可以結合debounceTail的功能,其可以實現最後一次操作的捕捉,如圖所示:
圖3,throttle加強運行圖
其代碼如下:
// 限頻,每delay的時間執行一次 缺點:如果可能無法捕獲最後一次結果
function throttle(fn, delay, ctx) {
let isAvail = true
let count = false
let movement = null
return function() {
count = true
let args = arguments
if (isAvail) {
fn.apply(ctx, args)
isAvail = false
count = false
setTimeout(function() {
isAvail = true
}, delay)
}
if (count) {
clearTimeout(movement)
movement = setTimeout(function() {
fn.apply(ctx, args)
}, 2 * delay)
}
}
}
增加movement來記錄和清除最終操作狀態;用count來避免與限頻的重合;如此便實現了捕獲最終操作狀態的限頻操作。
tips:其中大量使用setTimeout()的操作,在高級瀏覽器中,可以使用requestAnimationFrame來替代setTimeout操作,從而提高性能。requestAnimationFrame的原理、優勢及低版本的兼容,可以查閱張鑫旭的博客,寫得很詳細:CSS3動畫那麽強,requestAnimationFrame還有毛線用?
性能提速:debounce(防抖)、throttle(節流/限頻)