跳到主要内容

防抖和节流

问题

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

答案

防抖(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);
}
};
}

关键点

  1. 使用闭包保存定时器引用
  2. 每次触发时先清除之前的定时器
  3. immediate 参数控制是否首次立即执行
  4. 使用 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 时间内至少执行一次,避免长时间不响应。

相关链接