防抖和节流
问题
什么是防抖和节流?它们的应用场景有哪些?如果要在时间刚开始就执行一次,应如何处理?
答案
防抖(Debounce)和节流(Throttle)都是用于控制函数执行频率的技术,主要用于性能优化。
防抖(Debounce)
核心思想
事件触发后,等待一段时间再执行。如果在等待期间事件再次触发,则重新计时。
类比:电梯关门——有人进来就重新等待,直到一段时间没人进来才关门。
function debounce<T extends (...args: any[]) => any>(
fn: T,
delay: number
): (...args: Parameters<T>) => void {
let timer: ReturnType<typeof setTimeout> | null = null;
return function (this: any, ...args: Parameters<T>) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
应用场景:
| 场景 | 说明 |
|---|---|
| 搜索框输入 | 用户停止输入后再发送请求 |
| 窗口 resize | 调整完成后再计算布局 |
| 表单验证 | 用户停止输入后再验证 |
| 按钮防重复点击 | 防止用户快速多次点击 |
节流(Throttle)
核心思想
在一段时间内,无论触发多少次事件,只执行一次。
类比:水龙头——无论怎么拧,水流速度有上限。
时间戳版本
function throttle<T extends (...args: any[]) => any>(
fn: T,
delay: number
): (...args: Parameters<T>) => void {
let lastTime = 0;
return function (this: any, ...args: Parameters<T>) {
const now = Date.now();
if (now - lastTime >= delay) {
fn.apply(this, args);
lastTime = now;
}
};
}
特点
- 首次触发立即执行
- 最后一次触发如果在时间间隔内,不会执行
定时器版本
function throttle<T extends (...args: any[]) => any>(
fn: T,
delay: number
): (...args: Parameters<T>) => void {
let timer: ReturnType<typeof setTimeout> | null = null;
return function (this: any, ...args: Parameters<T>) {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
}
};
}
特点
- 首次触发不会立即执行,而是等待 delay 后执行
- 最后一次触发一定会执行(延迟执行)
两种版本对比
| 特性 | 时间戳版本 | 定时器版本 |
|---|---|---|
| 首次触发 | 立即执行 | 延迟执行 |
| 最后一次触发 | 可能不执行 | 一定执行 |
| 实现方式 | Date.now() 比较 | setTimeout |
应用场景:
| 场景 | 说明 |
|---|---|
| 滚动事件监听 | 滚动时定期检查位置(如懒加载) |
| 鼠标移动 | 拖拽时定期更新位置 |
| 游戏中按键 | 限制技能释放频率 |
| 实时搜索建议 | 限制请求频率 |
立即执行版本
如果需要在时间刚开始就执行一次,可以添加 immediate 参数:
防抖立即执行版
function debounce<T extends (...args: any[]) => any>(
fn: T,
delay: number,
immediate: boolean = false
): (...args: Parameters<T>) => void {
let timer: ReturnType<typeof setTimeout> | null = null;
return function (this: any, ...args: Parameters<T>) {
const callNow = immediate && !timer;
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
if (!immediate) {
fn.apply(this, args);
}
}, delay);
// 立即执行
if (callNow) {
fn.apply(this, args);
}
};
}
// 使用示例
const handleClick = debounce(
() => {
console.log('clicked');
},
1000,
true // 第三个参数为 true,立即执行
);
节流立即执行版
function throttle<T extends (...args: any[]) => any>(
fn: T,
delay: number,
immediate: boolean = true
): (...args: Parameters<T>) => void {
let lastTime = 0;
return function (this: any, ...args: Parameters<T>) {
const now = Date.now();
// 第一次是否立即执行
if (lastTime === 0 && !immediate) {
lastTime = now;
}
if (now - lastTime >= delay) {
fn.apply(this, args);
lastTime = now;
}
};
}
节流完整版(支持首次和结束时执行)
interface ThrottleOptions {
leading?: boolean; // 是否在开始时执行
trailing?: boolean; // 是否在结束时执行
}
function throttle<T extends (...args: any[]) => any>(
fn: T,
delay: number,
options: ThrottleOptions = {}
): (...args: Parameters<T>) => void {
let timer: ReturnType<typeof setTimeout> | null = null;
let lastTime = 0;
const { leading = true, trailing = true } = options;
return function (this: any, ...args: Parameters<T>) {
const now = Date.now();
// 首次不执行时,将 lastTime 设为当前时间
if (lastTime === 0 && !leading) {
lastTime = now;
}
const remaining = delay - (now - lastTime);
if (remaining <= 0) {
if (timer) {
clearTimeout(timer);
timer = null;
}
fn.apply(this, args);
lastTime = now;
} else if (!timer && trailing) {
// 设置定时器,确保结束后执行一次
timer = setTimeout(() => {
fn.apply(this, args);
lastTime = leading ? Date.now() : 0;
timer = null;
}, remaining);
}
};
}
// 使用示例
const handleScroll = throttle(
() => console.log('scrolling'),
1000,
{ leading: true, trailing: true }
);
对比总结
| 特性 | 防抖 (Debounce) | 节流 (Throttle) |
|---|---|---|
| 执行时机 | 事件停止后执行 | 固定间隔执行 |
| 执行次数 | 可能只执行一次 | 间隔内至少执行一次 |
| 适用场景 | 关注最终状态 | 关注过程中的状态 |
| 典型应用 | 搜索框、表单验证 | 滚动监听、拖拽 |
注意
- 防抖可能导致函数长时间不执行(如果事件一直触发)
- 节流保证函数在一定时间内至少执行一次
- 选择哪种方式取决于具体业务需求