防抖和節流
在前端開發的過程中,我們經常會需要綁定一些持續觸發的事件,如 resize、scroll、mousemove 等等,但有些時候我們并不希望在事件持續觸發的過程中那麼頻繁地去執行函數。一般來講,防抖和節流是比較好的解決方案。
讓我們先來看看在事件持續觸發的過程中頻繁執行函數是怎樣的一種情況。
html 檔案中代碼如下
<div id="content"
style="height:150px;line-height:150px;text-align:center; color: #fff;background-color:#ccc;font-size:80px;"></div>
<script>
let num = 1;
const content = document.getElementById('content');
function count() {
content.innerHTML = num++;
};
content.onmousemove = count;
</script>
在上述代碼中,div 元素綁定了 mousemove 事件,當滑鼠在 div(灰色)區域中移動的時候會持續地去觸發該事件導緻頻繁執行函數。效果如下
可以看到,在沒有通過其它操作的情況下,函數被頻繁地執行導緻頁面上資料變化特别快。是以,接下來讓我們來看看防抖和節流是如何去解決這個問題的。
防抖(debounce)
所謂防抖,就是指觸發事件後 n 秒後才執行函數,如果在 n 秒内又觸發了事件,則會重新計算函數執行時間。
适用場景:
- 按鈕送出場景:防止多次送出按鈕,隻執行最後送出的一次
- 搜尋框聯想場景:防止聯想發送請求,隻發送最後一次輸入
- 滑鼠移動綁定數字累加)
防抖函數分為非立即執行版和立即執行版。
非立即執行版:
function debounce(func, wait) {
let timeout;
return function () {
const context = this;
const args = [...arguments];
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(context, args)
}, wait);
}
}
非立即執行版的意思是觸發事件後函數不會立即執行,而是在 n 秒後執行,如果在 n 秒内又觸發了事件,則會重新計算函數執行時間。
我們依舊使用上述綁定 mousemove 事件的例子,通過上面的防抖函數,我們可以這麼使用
content.onmousemove = debounce(count,1000);
效果如下
可以看到,在觸發事件後函數 1 秒後才執行,而如果我在觸發事件後的 1 秒内又觸發了事件,則會重新計算函數執行時間。
上述防抖函數的代碼還需要注意的是 this 和 參數的傳遞
const context = this;
const args = [...arguments];
防抖函數的代碼使用這兩行代碼來擷取 this 和 參數,是為了讓 debounce 函數最終傳回的函數 this 指向不變以及依舊能接受到 e 參數。
立即執行版:
function debounce(func,wait) {
let timeout;
return function () {
const context = this;
const args = [...arguments];
if (timeout) clearTimeout(timeout);
const callNow = !timeout;
timeout = setTimeout(() => {
timeout = null;
}, wait)
if (callNow) func.apply(context, args)
}
}
立即執行版的意思是觸發事件後函數會立即執行,然後 n 秒内不觸發事件才能繼續執行函數的效果。
使用方法同上,效果如下
在開發過程中,我們需要根據不同的場景來決定我們需要使用哪一個版本的防抖函數,一般來講上述的防抖函數都能滿足大部分的場景需求。但我們也可以将非立即執行版和立即執行版的防抖函數結合起來,實作最終的雙劍合璧版的防抖函數。
雙劍合璧版:
/**
* @desc 函數防抖
* @param func 函數
* @param wait 延遲執行毫秒數
* @param immediate true 表立即執行,false 表非立即執行
*/
function debounce(func, wait, immediate) {
let timeout;
return function () {
const context = this;
const args = [...arguments];
if (timeout) clearTimeout(timeout);
if (immediate) {
const callNow = !timeout;
timeout = setTimeout(() => {
timeout = null;
}, wait)
if (callNow) func.apply(context, args)
}
else {
timeout = setTimeout(() => {
func.apply(context, args)
}, wait);
}
}
}
節流(throttle)
所謂節流,就是指連續觸發事件但是在 n 秒中隻執行一次函數。 節流會稀釋函數的執行頻率。
适用場景
- 拖拽場景:固定時間内隻執行一次,防止超高頻次觸發位置變動
- 縮放場景:監控浏覽器resize
對于節流,一般有兩種方式可以實作,分别是時間戳版和定時器版。
時間戳版:
function throttle(func, wait) {
var previous = 0;
return function() {
let now = Date.now();
let context = this;
let args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}
}
使用方式如下
content.onmousemove = throttle(count,1000);
效果如下
可以看到,在持續觸發事件的過程中,函數會立即執行,并且每 1s 執行一次。
定時器版:
function throttle(func, wait) {
let timeout;
return function() {
let context = this;
let args = arguments;
if (!timeout) {
timeout = setTimeout(() => {
timeout = null;
func.apply(context, args)
}, wait)
}
}
}
使用方式同上,效果如下
可以看到,在持續觸發事件的過程中,函數不會立即執行,并且每 1s 執行一次,在停止觸發事件後,函數還會再執行一次。
我們應該可以很容易的發現,其實時間戳版和定時器版的節流函數的差別就是,時間戳版的函數觸發是在時間段内開始的時候,而定時器版的函數觸發是在時間段内結束的時候。
同樣地,我們也可以将時間戳版和定時器版的節流函數結合起來,實作雙劍合璧版的節流函數。
雙劍合璧版:
/**
* @desc 函數節流
* @param func 函數
* @param wait 延遲執行毫秒數
* @param type 1 表時間戳版,2 表定時器版
*/
function throttle(func, wait ,type) {
if(type===1){
let previous = 0;
}else if(type===2){
let timeout;
}
return function() {
let context = this;
let args = arguments;
if(type===1){
let now = Date.now();
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}else if(type===2){
if (!timeout) {
timeout = setTimeout(() => {
timeout = null;
func.apply(context, args)
}, wait)
}
}
}
}
原文連結:https://juejin.cn/post/6844903651278848014