前言
在前端開發中會遇到一些頻繁的事件觸發,比如:
- window 的 resize、scroll
- mousedown、mousemove、mousewheel(滑鼠滾輪)
-
keyup(彈起鍵盤)、keydown(按下鍵盤)、keypress(按下字元鍵盤)
……
想象一下視窗的resize事件或者是一個元素的onmouseover事件 - 他們觸發時,執行的非常迅速,并且觸發很多次。如果你的回調過重,你可能使浏覽器死掉。
這就是為什麼要使用防抖。
原理
防抖的原理:在wait時間内,持續觸發某個事件。第一種情況:如果某個事件觸發wait秒内又觸發了該事件,就應該以新的事件wait等待時間為準,wait秒後再執行此事件;第二種情況:如果某個事件觸發wait秒後,未再觸發該事件,則在wait秒後直接執行該事件。
通俗一點:定義wait=2000,持續點選按鈕,前後點選間隔都在2秒内,則在最後一次點選按鈕後,等待2秒再執行func方法。如果點選完按鈕,2秒後未再次點選按鈕,則2秒後直接執行func方法。
示例代碼
代碼一(根據原理)
定義函數debounce
根據表述,我們可以知道需要傳入參數:func、wait
實作代碼:
function debounce(func, wait = 500) {
let timeout; // 定義定時器,wait秒後需要清除定時器
return function () {
// 如果再次觸發函數時,已有timeout,則清空銷毀目前timeout,再以新的事件重新設定定時器
if (timeout) clearTimeout(timeout);
timeout = setTimeout(function () {
func();
clearTimeout(timeout)
}, wait);
};
}
代碼二(解決函數this指向)
我們之前的原函數指向哪,如果使用我們的 debounce 函數包裹後,也要将this指向正确的對象。
function debounce(func, wait = 500) {
let timeout, context;
return function () {
context = this;
// 如果再次觸發函數時,已有timeout,則清空銷毀timeout,在往下執行
if (timeout) clearTimeout(timeout);
timeout = setTimeout(function () {
func.apply(context);
clearTimeout(timeout);
}, wait);
};
}
代碼三(解決函數event對象)
JavaScript 在事件處理函數中會提供事件對象 event;
是以,也要考慮到保持原函數的event對象相同
式一:
function debounce(func, wait = 500) {
let timeout, context, args;
return function () {
context = this;
args = arguments;
// 如果再次觸發函數時,已有timeout,則清空銷毀timeout,在往下執行
if (timeout) clearTimeout(timeout);
timeout = setTimeout(function () {
func.apply(context, args);
clearTimeout(timeout);
}, wait);
};
}
式二:
function debounce(func, wait = 500) {
let timeout, context;
return function (...args) {
context = this;
// 如果再次觸發函數時,已有timeout,則清空銷毀timeout,在往下執行
if (timeout) clearTimeout(timeout);
timeout = setTimeout(function () {
func.apply(context, args);
clearTimeout(timeout);
}, wait);
};
}
代碼四(函數傳回值)
此時需要注意一個問題,就是我們在執行原函數時可能有傳回值,我們需要處理debounce函數,在最後也要有相同傳回值。
這裡做出的處理,是将
func.apply(context, args)
單獨拿出來,輸出原函數的
result
。
function debounce(func, wait = 500) {
let timeout, context, result;
function showResult(e1, e2) {
result = func.apply(e1, e2); // 綁定e1,e2的同時,輸出result
return result;
}
return function (...args) {
context = this;
// 如果再次觸發函數時,已有timeout,則清空銷毀timeout,在往下執行
if (timeout) clearTimeout(timeout);
// 這裡是不立即執行的原代碼
timeout = setTimeout(function () {
clearTimeout(timeout);
return showResult(context, args); // 将this,arguments代入函數
}, wait);
return result
};
}
代碼五(立刻執行)
因為原理中,每次觸發完後還需要等待wait秒執行。
但是某些場景,比如按鈕點選後調用接口,會使整個時間變長,這時候就需要定義immediate,點選按鈕,立即執行調用接口,還要達到wait秒内防抖的效果。
function debounce(func, wait = 500, immediate = false) {
let timeout, context, result, callNow;
function showResult(e1, e2) {
result = func.apply(e1, e2); // 綁定e1,e2的同時,輸出result
return result;
}
return function (...args) {
context = this;
// 如果再次觸發函數時,已有timeout,則清空銷毀timeout,在往下執行
if (timeout) clearTimeout(timeout);
if (immediate) {
// 這裡是立即執行的判斷代碼
callNow = !timeout; // timeout最開始定義為undefined,如果未設定定時器,則!timeout傳回true;否則傳回false
timeout = setTimeout(function () {
timeout = null; // 這裡時定時器走完,讓timeout為null,則上一步!timeout依然傳回true;
}, wait);
if (callNow) return showResult(context, args); //剛進入timeout=undefined以及,wait時間走完timeout = null,兩種情況都會立即執行函數
} else {
// 這裡是不立即執行的原代碼
timeout = setTimeout(function () {
clearTimeout(timeout);
return showResult(context, args); // 将this,arguments代入函數
}, wait);
}
return result
};
}
代碼六(取消)
增加取消防抖的方法:隻需要定義cancel方法,去除定時器,将初始變量全部設定為undefined。
function debounce(func, wait = 500, immediate = false) {
let timeout, context, result, callNow;
function showResult(e1, e2) {
result = func.apply(e1, e2); // 綁定e1,e2的同時,輸出result
return result;
}
const debounced = function (...args) {
context = this;
// 如果再次觸發函數時,已有timeout,則清空銷毀timeout,在往下執行
if (timeout) clearTimeout(timeout);
if (immediate) {
// 這裡是立即執行的判斷代碼
callNow = !timeout; // timeout最開始定義為undefined,如果未設定定時器,則!timeout傳回true;否則傳回false
timeout = setTimeout(function () {
timeout = null; // 這裡時定時器走完,讓timeout為null,則上一步!timeout依然傳回true;
}, wait);
if (callNow) return showResult(context, args); //剛進入timeout=undefined以及,wait時間走完timeout = null,兩種情況都會立即執行函數
} else {
// 這裡是不立即執行的原代碼
timeout = setTimeout(function () {
return showResult(context, args); // 将this,arguments代入函數
clearTimeout(timeout);
}, wait);
}
return result
};
debounced.cancel = function () {
// 去除定時器,
if (timeout !== undefined) {
clearTimeout(timeout);
}
// 将初始變量全部設定為undefined
timeout = context = result = callNow = undefined;
};
return debounced;
}
示範位址
可以去Github倉庫檢視示範代碼
跟着大佬學系列
主要是日常對每個進階知識點的摸透,跟着大佬一起去深入了解JavaScript的語言藝術。
後續會一直更新,希望各位看官不要吝啬手中的贊。
❤️ 感謝各位的支援!!!
參考
- JavaScript專題之跟着underscore學防抖
- underscore.js
- 深入淺出防抖函數 debounce