首先要明白 節流 Throttle 和 去抖動 Debounce 兩者是有區別的,很多人一開始都會搞混。
先講講去抖動 Debounce
Debounce
為什么要去抖動?
我們知道 瀏覽器有一些原生事件,比如 resize scroll keyup keydown 這些事件的回調函數,當他們觸發的時候,并不是想象中的只觸發一次,而是幾次甚至幾十次,如果當你的這些事件回調函數中有一些復雜的運算或者dom操作,低配瀏覽器很容易出現假死的狀態。
去抖動Debounce實現的效果是:以scroll來舉例,當scroll回調在指定的時間n毫秒內還會觸發,此次回調方法不執行,繼續等待n毫秒,直到n毫秒之后此方法不再觸發,執行這個方法。簡單來說就是:把在指定時間內可能會多次執行的方法打包成一次。
window.debounce = function(fun,dely){ //fun 需要去抖動的方法,dely 指定的延遲時間
var timer = null; // 用閉包維護一個timer 做為定時器標識
return function(){
var context = this; // 調用debounce的時候 保存執行上下文
var args = arguments;
clearTimeout(timer);
timer = setTimeout(function() {
fun.apply(context , args);
}, delay); // 設定定時器 判斷是否已經觸發 ,如果觸發則重新計時 等待dely毫秒再執行
}
}
此時如果調用
foo = function(){
console.log('scroll work')
}
dom.addEventListener('scroll', debounce(foo, 2000)); // 當dom連續觸發scroll 時 回調函數只會在兩秒后執行一次
但是這種寫法有一個明顯的缺陷,就是當用戶觸發的第一時間方法是不會調用,所以上升級版
window.debounce = function(fun,dely){
var timer = null;
return function(){
var context = this;
var args = arguments;
if(timer) { clearTimeout(timer) }; // 看似多余的 但是是必須的 讀者可以自己思考為什么需要這么處理
var doNow = !timer; // 判斷是否有定時器,如果有,就dely后清除timer,否則立即執行;
timer = setTimeou(function(){
timer = null ;
},dely)
if(doNow){
fun.apply(context, args);
}
}
}
現在的效果是,你滾動的第一時間會觸發回調,然后你要是連續再觸發,在dely秒之內是不會觸發的,只有等dely毫秒后 timer 清除了,再觸發滾動才會調用回調。
想必兩個版本的問題大家都看出來了,多多少少都是有點奇怪。接下來就是節流登場了
Throttle
節流函數是處理類似場景但抖動不適合的另一種解決方案,比如大型電商網站當用戶滾動到頁面底部的時候再發AJAX請求獲取圖片,實現圖片懶加載,如果使用去抖動,不管方案一還是二,都會用種奇怪的體驗,假設設置500ms的delay時間,使用方案一,效果則是,用戶滾動了,500ms后發AJAX獲取圖片,再顯示圖片。期間500ms用戶是只能看到圖片缺失的。如果使用方案二,似乎是能實現需求,但是仔細想想,如果用戶不是500ms滾動一次,而是玩命的在連續滾動,則AJAX只會觸發一次,用戶只能看到第一次滾動觸發AJAX返回的圖片,后面的則是圖片缺失狀態。
到這應該可以猜到節流實現的什么效果了。
節流函數允許一個函數在規定的時間內只執行一次。
它和防抖動最大的區別就是,節流函數不管事件觸發有多頻繁,都會保證在規定時間內一定會執行一次真正的事件處理函數。
主要有兩種實現方法:
1.時間戳
2.定時器
時間戳實現:
window.throttle = function(fun,delay){
var prev = Date.now(); // 閉包維護一個起始時間戳
return function(){
var context = this;
var args = arguments;
var now = Date.now(); // 每次任務函數觸發的時候獲取時間戳
if(now-prev>=delay){ // 判斷當前時間與起始時間戳的間隔 大余delay則觸發任務函數
fun.apply(context,args);
prev = Date.now(); // 關鍵是要更新閉包中的 起始時間戳
}
}
}
此時我們再測試
foo = function(){
console.log('scroll work')
}
dom.addEventListener('scroll', throttle (foo, 1000)); // 當dom連續觸發scroll 時 任務函數每隔1秒也會觸發一次,當然眼尖朋友會發現有個小瑕疵
定時器實現:
var throttle = function(fun,delay){
var timer = null; // 維護一個定時器
return function(){
var context = this;
var args = arguments;
if(!timer){ // 當任務函數觸發了 , 判斷定時器是否存在 不存在才執行任務函數
timer = setTimeout(function(){
fun.apply(context,args);
timer = null;
},delay); // 當定時器不存在的時候 delay秒后才執行任務函數 并且清空定時器 接著下個輪回
}
}
}
當第一次觸發事件時,肯定不會立即執行函數,而是在delay秒后才執行。
之后連續不斷觸發事件,也會每delay秒執行一次。
當最后一次停止觸發后,由于定時器的delay延遲,可能還會執行一次函數。
可以綜合使用時間戳與定時器,完成一個事件觸發時立即執行,觸發完畢還能執行一次的節流函數:
window.throttle = function(fun,delay){
var timer = null;
var startTime = Date.now();
return function(){
var curTime = Date.now();
var remaining = delay-(curTime-startTime); // 計算出兩次觸發的時間間隔有沒有大余delay
var context = this;
var args = arguments;
clearTimeout(timer);
if(remaining<=0){
func.apply(context,args);
startTime = Date.now(); // 如果兩次觸發時間大余delay,則立馬觸發一次任務函數并且更新起始時間戳
}else{
timer = setTimeout(fun,remaining); // 如果兩次觸發時間小于delay, 則改變定時器時間保證delay時間一定觸發任務函數
}
}
}
總結
防止一個事件頻繁觸發回調函數的方式:
防抖動debounce:將幾次操作合并為一此操作進行。原理是維護一個計時器,規定在delay時間后觸發函數,但是在delay時間內再次觸發的話,就會取消之前的計時器而重新設置。這樣一來,只有最后一次操作能被觸發。
節流throttle :使得一定時間內只觸發一次函數。
它和防抖動最大的區別就是,節流函數不管事件觸發有多頻繁,都會保證在規定時間內一定會執行一次真正的事件處理函數,而防抖動只是在最后一次事件后才觸發一次函數。
原理是通過判斷是否到達一定時間來觸發函數,若沒到規定時間則使用計時器延后,而下一次事件則會重新設定計時器。
---end