一、防抖的概念
防抖:当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次。如果设定的时间到来之前又一次触发了事件,就重新开始计时。
典型应用场景:
- 搜索框输入联想(等待用户停止输入后再请求)
- 窗口大小调整(等待调整结束后再计算布局)
- 按钮防重复点击
二、基础实现思路
-
基本需求:
- 延迟执行函数
- 如果在延迟期间再次触发,则取消前一次的执行
- 只执行最后一次触发
-
实现要素:
- 需要一个定时器来管理延迟执行
- 需要保存函数执行的上下文(this)和参数
- 需要清除前一个定时器的能力
三、一步步实现
第一步:基础框架
javascript
function debounce(func, wait) {
let timeout;
return function() {
// 保存上下文和参数
const context = this;
const args = arguments;
// 清除之前的定时器
clearTimeout(timeout);
// 设置新的定时器
timeout = setTimeout(function() {
func.apply(context, args);
}, wait);
};
}
第二步:立即执行选项
有时候我们需要第一次触发时立即执行,然后等待停止触发后再恢复。
javascript
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
timeout = setTimeout(function() {
timeout = null;
}, wait);
if (callNow) func.apply(context, args);
}
// 非立即执行的情况
else {
timeout = setTimeout(function() {
func.apply(context, args);
}, wait);
}
};
}
第三步:取消功能
有时候我们需要能够在等待期间手动取消防抖。
javascript
function debounce(func, wait, immediate) {
let timeout;
// 返回的防抖函数
const debounced = function() {
const context = this;
const args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
const callNow = !timeout;
timeout = setTimeout(function() {
timeout = null;
}, wait);
if (callNow) func.apply(context, args);
} else {
timeout = setTimeout(function() {
func.apply(context, args);
}, wait);
}
};
// 添加取消方法
debounced.cancel = function() {
clearTimeout(timeout);
timeout = null;
};
return debounced;
}
第四步:返回值处理
当被防抖的函数有返回值时,我们需要处理返回值。对于立即执行的情况,可以返回函数执行结果。
javascript
function debounce(func, wait, immediate) {
let timeout, result;
const debounced = function() {
const context = this;
const args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
const callNow = !timeout;
timeout = setTimeout(function() {
timeout = null;
}, wait);
if (callNow) result = func.apply(context, args);
} else {
timeout = setTimeout(function() {
func.apply(context, args);
}, wait);
}
return result;
};
debounced.cancel = function() {
clearTimeout(timeout);
timeout = null;
};
return debounced;
}
第五步:完整实现(带注释)
javascript
/**
* 防抖函数
* @param {Function} func 需要防抖的函数
* @param {number} wait 等待时间(毫秒)
* @param {boolean} immediate 是否立即执行
* @return {Function} 返回防抖后的函数
*/
function debounce(func, wait, immediate) {
let timeout, result;
const debounced = function() {
const context = this;
const args = arguments;
// 如果已有定时器存在,则清除
if (timeout) clearTimeout(timeout);
// 立即执行模式
if (immediate) {
// 如果定时器不存在,表示可以立即执行
const callNow = !timeout;
// 设置定时器,在等待时间后重置timeout
timeout = setTimeout(() => {
timeout = null;
}, wait);
// 如果允许立即执行,则调用函数
if (callNow) result = func.apply(context, args);
}
// 非立即执行模式
else {
// 设置定时器,在等待时间后执行函数
timeout = setTimeout(() => {
func.apply(context, args);
}, wait);
}
return result;
};
// 添加取消方法
debounced.cancel = function() {
clearTimeout(timeout);
timeout = null;
};
return debounced;
}
四、使用示例
1. 基本使用
javascript
// 需要防抖的函数
function search(query) {
console.log('搜索:', query);
}
// 创建防抖函数
const debouncedSearch = debounce(search, 500);
// 模拟连续输入
debouncedSearch('a');
debouncedSearch('ab');
debouncedSearch('abc');
// 只有最后一次调用会在500ms后执行,输出: 搜索: abc
2. 立即执行模式
javascript
function buttonClick() {
console.log('按钮点击');
}
const debouncedClick = debounce(buttonClick, 1000, true);
// 第一次点击会立即执行
debouncedClick(); // 立即输出: 按钮点击
// 1秒内连续点击不会触发
debouncedClick();
debouncedClick();
// 1秒后再次点击会再次立即执行
3. 取消防抖
javascript
function saveData() {
console.log('数据已保存');
}
const debouncedSave = debounce(saveData, 2000);
debouncedSave(); // 开始计时
// 1秒后取消
setTimeout(() => {
debouncedSave.cancel();
console.log('保存已取消');
}, 1000);
// 不会输出"数据已保存"
五、实际应用场景
1. 搜索框联想
javascript
const searchInput = document.getElementById('search-input');
function fetchSuggestions(query) {
// 发送请求获取搜索建议
console.log('获取建议:', query);
}
const debouncedFetch = debounce(fetchSuggestions, 300);
searchInput.addEventListener('input', function(e) {
debouncedFetch(e.target.value);
});
2. 窗口大小调整
javascript
function handleResize() {
console.log('窗口大小:', window.innerWidth, window.innerHeight);
}
window.addEventListener('resize', debounce(handleResize, 250));
3. 按钮防重复点击
javascript
const submitBtn = document.getElementById('submit-btn');
function submitForm() {
console.log('表单提交');
// 实际提交逻辑...
}
submitBtn.addEventListener('click', debounce(submitForm, 1000, true));
六、与节流(throttle)的区别
特性 | 防抖(debounce) | 节流(throttle) |
---|---|---|
执行时机 | 停止触发后延迟执行 | 固定时间间隔执行 |
重置机制 | 每次触发都会重置计时器 | 计时器期间新触发会被忽略 |
适用场景 | 搜索联想、窗口resize结束后的操作 | 滚动事件、鼠标移动事件等频繁触发 |
七、TypeScript 版本实现
typescript
interface DebouncedFunction<T extends (...args: any[]) => any> {
(...args: Parameters<T>): ReturnType<T> | undefined;
cancel: () => void;
}
function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number,
immediate?: boolean
): DebouncedFunction<T> {
let timeout: ReturnType<typeof setTimeout> | null;
let result: ReturnType<T> | undefined;
const debounced = function(this: any, ...args: Parameters<T>) {
const context = this;
if (timeout) clearTimeout(timeout);
if (immediate) {
const callNow = !timeout;
timeout = setTimeout(() => {
timeout = null;
}, wait);
if (callNow) result = func.apply(context, args);
} else {
timeout = setTimeout(() => {
func.apply(context, args);
}, wait);
}
return result;
} as DebouncedFunction<T>;
debounced.cancel = () => {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
};
return debounced;
}
八、总结
- 防抖核心:通过定时器延迟执行,在频繁触发时只执行最后一次
- 关键点 :
- 使用闭包保存定时器变量
- 正确处理执行上下文(this)和参数
- 提供取消功能增强灵活性
- 可选立即执行模式满足不同需求
- 应用场景:高频事件但只需要最终状态的场景
- 注意事项 :
- 合理设置等待时间
- 注意内存泄漏问题(及时取消)
- 对于需要返回值的情况要特殊处理