在前端開發的過程中,我們經常會需要綁定一些持續觸發的事件,如 resize、scroll、mousemove 等等,但有些時候我們并不希望在事件持續觸發的過程中那么頻繁地去執行函數。
通常這種情況下我們怎么去解決的呢?一般來講,防抖和節流是比較好的解決方案。
讓我們先來看看在事件持續觸發的過程中頻繁執行函數是怎樣的一種情況。
html 文件中代碼如下
letnum =1;letcontent =document.getElementById('content');functioncount(){? ? ? ? content.innerHTML = num++;? ? };? ? content.onmousemove = count;
在上述代碼中,div 元素綁定了 mousemove 事件,當鼠標在 div(灰色)區域中移動的時候會持續地去觸發該事件導致頻繁執行函數。效果如下
可以看到,在沒有通過其它操作的情況下,函數被頻繁地執行導致頁面上數據變化特別快。所以,接下來讓我們來看看防抖和節流是如何去解決這個問題的。
防抖(debounce)
所謂防抖,就是指觸發事件后在 n 秒內函數只能執行一次,如果在 n 秒內又觸發了事件,則會重新計算函數執行時間。
防抖函數分為非立即執行版和立即執行版。
非立即執行版:
functiondebounce(func, wait){lettimeout;returnfunction(){letcontext =this;letargs =arguments;if(timeout) clearTimeout(timeout);? ? ? ? ? ? ? ? timeout = setTimeout(()=>{? ? ? ? ? ? func.apply(context, args)? ? ? ? }, wait);? ? }}
非立即執行版的意思是觸發事件后函數不會立即執行,而是在 n 秒后執行,如果在 n 秒內又觸發了事件,則會重新計算函數執行時間。
我們依舊使用上述綁定 mousemove 事件的例子,通過上面的防抖函數,我們可以這么使用
content.onmousemove = debounce(count,1000);
效果如下
可以看到,在觸發事件后函數 1 秒后才執行,而如果我在觸發事件后的 1 秒內又觸發了事件,則會重新計算函數執行時間。
上述防抖函數的代碼還需要注意的是 this 和 參數的傳遞
letcontext =this;letargs =arguments;
防抖函數的代碼使用這兩行代碼來獲取 this 和 參數,是為了讓 debounce 函數最終返回的函數 this 指向不變以及依舊能接受到 e 參數。
立即執行版:
functiondebounce(func,wait){lettimeout;returnfunction(){letcontext =this;letargs =arguments;if(timeout) clearTimeout(timeout);letcallNow = !timeout;? ? ? ? timeout = setTimeout(()=>{? ? ? ? ? ? timeout =null;? ? ? ? }, wait)if(callNow) func.apply(context, args)? ? }}
立即執行版的意思是觸發事件后函數會立即執行,然后 n 秒內不觸發事件才能繼續執行函數的效果。
使用方法同上,效果如下
在開發過程中,我們需要根據不同的場景來決定我們需要使用哪一個版本的防抖函數,一般來講上述的防抖函數都能滿足大部分的場景需求。但我們也可以將非立即執行版和立即執行版的防抖函數結合起來,實現最終的雙劍合璧版的防抖函數。
雙劍合璧版:
/**
* @desc 函數防抖
* @param func 函數
* @param wait 延遲執行毫秒數
* @param immediate true 表立即執行,false 表非立即執行
*/functiondebounce(func,wait,immediate){lettimeout;returnfunction(){letcontext =this;letargs =arguments;if(timeout) clearTimeout(timeout);if(immediate) {varcallNow = !timeout;? ? ? ? ? ? timeout = setTimeout(()=>{? ? ? ? ? ? ? ? timeout =null;? ? ? ? ? ? }, wait)if(callNow) func.apply(context, args)? ? ? ? }else{? ? ? ? ? ? timeout = setTimeout(function(){? ? ? ? ? ? ? ? func.apply(context, args)? ? ? ? ? ? }, wait);? ? ? ? }? ? }}
節流(throttle)
所謂節流,就是指連續觸發事件但是在 n 秒中只執行一次函數。節流會稀釋函數的執行頻率。
對于節流,一般有兩種方式可以實現,分別是時間戳版和定時器版。
時間戳版:
functionthrottle(func, wait){letprevious =0;returnfunction(){letnow =Date.now();letcontext =this;letargs =arguments;if(now - previous > wait) {? ? ? ? ? ? func.apply(context, args);? ? ? ? ? ? previous = now;? ? ? ? }? ? }}
使用方式如下
content.onmousemove = throttle(count,1000);
效果如下
可以看到,在持續觸發事件的過程中,函數會立即執行,并且每 1s 執行一次。
定時器版:
functionthrottle(func, wait){lettimeout;returnfunction(){letcontext =this;letargs =arguments;if(!timeout) {? ? ? ? ? ? timeout = setTimeout(()=>{? ? ? ? ? ? ? ? timeout =null;? ? ? ? ? ? ? ? func.apply(context, args)? ? ? ? ? ? }, wait)? ? ? ? }? ? }}
使用方式同上,效果如下
可以看到,在持續觸發事件的過程中,函數不會立即執行,并且每 1s 執行一次,在停止觸發事件后,函數還會再執行一次。
我們應該可以很容易的發現,其實時間戳版和定時器版的節流函數的區別就是,時間戳版的函數觸發是在時間段內開始的時候,而定時器版的函數觸發是在時間段內結束的時候。
同樣地,我們也可以將時間戳版和定時器版的節流函數結合起來,實現雙劍合璧版的節流函數。
雙劍合璧版:
/**
* @desc 函數節流
* @param func 函數
* @param wait 延遲執行毫秒數
* @param type 1 表時間戳版,2 表定時器版
*/functionthrottle(func, wait ,type){if(type===1){letprevious =0;? ? }elseif(type===2){lettimeout;? ? }returnfunction(){letcontext =this;letargs =arguments;if(type===1){letnow =Date.now();if(now - previous > wait) {? ? ? ? ? ? ? ? func.apply(context, args);? ? ? ? ? ? ? ? previous = now;? ? ? ? ? ? }? ? ? ? }elseif(type===2){if(!timeout) {? ? ? ? ? ? ? ? timeout = setTimeout(()=>{? ? ? ? ? ? ? ? ? ? timeout =null;? ? ? ? ? ? ? ? ? ? func.apply(context, args)? ? ? ? ? ? ? ? }, wait)? ? ? ? ? ? }? ? ? ? }? ? }}