算法 - 防抖 (Debounce) 手写实现

1. 题目

实现一个防抖函数 debounce(fn, wait, immediate):

  • 在事件被频繁触发时,只有在停止触发 wait 毫秒后,fn 才真正执行一次。
  • 每次新触发都会「重新计时」(把上一次的待执行取消)。
  • immediate = true 时,首次触发立即执行一次,之后的连续触发不再执行,直到静默 wait 后重新武装。
  • 要求保留正确的 this 与参数,并提供 cancel 方法取消待执行。

典型场景:搜索框输入联想、窗口 resize 重排、按钮防连点、滚动监听。

示例:

1
2
用户连续输入 "typescript"(每 50ms 一个字符)
debounce(search, 300) -> 停止输入 300ms 后才发一次请求

2. 解题思路

一句话:用一个闭包持有的定时器 ID,每次触发先 clearTimeout 再重新 setTimeout。这样只有「最后一次触发」的定时器能活到时间并执行回调。

要点:

  1. timer 存在闭包里,外界看不见,只在函数内部读写。
  2. 执行回调时用 fn.apply(this, args),保证 this 指向调用方(比如事件绑定的 DOM),参数也原样透传。
  3. immediate 版本:若已立即执行过,就把 timer 设为一个哨兵值表示「冷却中」,静默期结束后置空以重新武装。
  4. cancel:清掉当前定时器并把状态复位。
  • 时间复杂度:每次触发 O(1)。
  • 空间:闭包一份。

3. TypeScript 实现

3.1 标准版(延迟执行,最常用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function debounce<T extends (...args: any[]) => void>(
fn: T,
wait: number
): ((...args: Parameters<T>) => void) & { cancel: () => void } {
let timer: ReturnType<typeof setTimeout> | null = null;

const debounced = function (this: ThisParameterType<T>, ...args: Parameters<T>) {
if (timer !== null) clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
fn.apply(this, args);
}, wait);
};

debounced.cancel = () => {
if (timer !== null) {
clearTimeout(timer);
timer = null;
}
};

return debounced;
}

3.2 支持 immediate(首次立即执行)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function debounceImmediate<T extends (...args: any[]) => void>(
fn: T,
wait: number,
immediate = false
) {
let timer: ReturnType<typeof setTimeout> | null = null;

return function (this: any, ...args: Parameters<T>) {
// 用 timer 是否存在判断「是否处于冷却」
if (timer !== null) clearTimeout(timer);

if (immediate) {
const callNow = timer === null; // 首次或冷却结束
timer = setTimeout(() => {
timer = null; // 重新武装
}, wait);
if (callNow) fn.apply(this, args);
} else {
timer = setTimeout(() => {
timer = null;
fn.apply(this, args);
}, wait);
}
};
}

// 用法
const onSearch = debounce((kw: string) => console.log("fetch:", kw), 300);
window.addEventListener("input", () => onSearch("abc"));
// 组件卸载 / 不再需要时:
onSearch.cancel();

4. 面试延伸

  • 防抖 vs 节流:防抖「等安静了再执行一次」,节流「固定频率最多执行一次」。二者常一起考,见下一篇 [节流]。
  • 带返回值怎么办? 防抖天然会推迟结果,直接 return 拿不到。工程做法:返回 Promise,或用回调;能讲清这个取舍很加分。
  • leading / trailing 配置:lodash 的 debounce 有 leading(是否先行执行)、maxWait(最长等待,防止永远不执行,此时退化为带节流的防抖)。
  • React 中要配合 useMemo/useRef 保存同一个 debounce 实例,否则每次渲染都新建、定时器互相打架。

难度:简单 | 前端手写题必考 | 事件频率控制基石