防抖和节流
问题
什么是防抖和节流?它们的应用场景有哪些?如果要在时间刚开始就执行一次,应如何处理?
答案
防抖(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) |
|---|---|---|
| 执行时机 | 事件停止后执行 | 固定间隔执行 |
| 执行次数 | 可能只执行一次 | 间隔内至少执行一次 |
| 适用场景 | 关注最终状态 | 关注过程中的状态 |
| 典型应用 | 搜索框、表单验证 | 滚动监听、拖拽 |
注意
- 防抖可能导致函数长时间不执行(如果事件一直触发)
- 节流保证函数在一定时间内至少执行一次
- 选择哪种方式取决于具体业务需求
常见面试问题
Q1: 防抖和节流的区别是什么?
答案:
| 对比项 | 防抖 (Debounce) | 节流 (Throttle) |
|---|---|---|
| 核心思想 | 等待一段时间没有新触发才执行 | 固定时间间隔内只执行一次 |
| 执行时机 | 事件停止触发后执行 | 在时间间隔内定期执行 |
| 执行次数 | 可能只执行最后一次 | 保证一定频率执行 |
| 类比 | 电梯关门等人 | 水龙头限流 |
简单记忆:
- 防抖:关注"最后一次",适合搜索框、表单验证
- 节流:关注"执行频率",适合滚动监听、拖拽
Q2: 如何实现一个防抖函数?请手写代码。
答案:
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);
}
};
}
关键点:
- 使用闭包保存定时器引用
- 每次触发时先清除之前的定时器
immediate参数控制是否首次立即执行- 使用
apply保持this上下文
Q3: 节流的时间戳版本和定时器版本有什么区别?
答案:
// 时间戳版本
function throttleTimestamp(fn: Function, delay: number) {
let lastTime = 0;
return function (...args: any[]) {
const now = Date.now();
if (now - lastTime >= delay) {
fn.apply(this, args);
lastTime = now;
}
};
}
// 定时器版本
function throttleTimer(fn: Function, delay: number) {
let timer: ReturnType<typeof setTimeout> | null = null;
return function (...args: any[]) {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
}
};
}
| 版本 | 首次触发 | 最后一次触发 |
|---|---|---|
| 时间戳 | ✅ 立即执行 | ❌ 可能不执行 |
| 定时器 | ❌ 延迟执行 | ✅ 一定执行 |
Q4: 如何实现一个既能首次执行又能末次执行的节流函数?
答案:
interface ThrottleOptions {
leading?: boolean; // 首次是否执行
trailing?: boolean; // 末次是否执行
}
function throttle<T extends (...args: any[]) => any>(
fn: T,
delay: number,
options: ThrottleOptions = {}
): (...args: Parameters<T>) => void {
const { leading = true, trailing = true } = options;
let timer: ReturnType<typeof setTimeout> | null = null;
let lastTime = 0;
return function (this: any, ...args: Parameters<T>) {
const now = Date.now();
if (!lastTime && !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);
}
};
}
Q5: 防抖和节流在 React 中如何正确使用?
答案:
在 React 中使用防抖/节流需要注意函数引用稳定性:
import { useMemo, useCallback, useRef, useEffect } from 'react';
// ❌ 错误:每次渲染都创建新的防抖函数
function BadExample() {
const handleSearch = debounce((value: string) => {
console.log(value);
}, 300);
// ...
}
// ✅ 正确:使用 useMemo 保持函数引用稳定
function GoodExample1() {
const handleSearch = useMemo(
() => debounce((value: string) => {
console.log(value);
}, 300),
[]
);
// ...
}
// ✅ 正确:使用 useRef 保存防抖函数
function GoodExample2() {
const debouncedFn = useRef(
debounce((value: string) => {
console.log(value);
}, 300)
).current;
// ...
}
// ✅ 最佳:使用 useLatest 避免闭包陷阱
function useDebounce<T extends (...args: any[]) => any>(fn: T, delay: number) {
const fnRef = useRef(fn);
fnRef.current = fn;
return useMemo(
() => debounce((...args: Parameters<T>) => fnRef.current(...args), delay),
[delay]
);
}
Q6: Lodash 的 debounce 有哪些高级选项?
答案:
import { debounce } from 'lodash';
const debouncedFn = debounce(fn, 300, {
leading: false, // 是否在延迟开始前执行(默认 false)
trailing: true, // 是否在延迟结束后执行(默认 true)
maxWait: 1000, // 最大等待时间(防止无限等待)
});
// 手动取消
debouncedFn.cancel();
// 立即执行(跳过延迟)
debouncedFn.flush();
maxWait 的作用:即使事件一直触发,也保证在 maxWait 时间内至少执行一次,避免长时间不响应。