跳到主要内容

防抖和节流

问题

什么是防抖和节流?它们的应用场景有哪些?如果要在时间刚开始就执行一次,应如何处理?

答案

防抖(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)
执行时机事件停止后执行固定间隔执行
执行次数可能只执行一次间隔内至少执行一次
适用场景关注最终状态关注过程中的状态
典型应用搜索框、表单验证滚动监听、拖拽
注意
  • 防抖可能导致函数长时间不执行(如果事件一直触发)
  • 节流保证函数在一定时间内至少执行一次
  • 选择哪种方式取决于具体业务需求

相关链接