算法 - 防抖 (Debounce) 手写实现
1. 题目
实现一个防抖函数 debounce(fn, wait, immediate):
- 在事件被频繁触发时,只有在停止触发
wait毫秒后,fn才真正执行一次。 - 每次新触发都会「重新计时」(把上一次的待执行取消)。
immediate = true时,首次触发立即执行一次,之后的连续触发不再执行,直到静默wait后重新武装。- 要求保留正确的
this与参数,并提供cancel方法取消待执行。
典型场景:搜索框输入联想、窗口 resize 重排、按钮防连点、滚动监听。
示例:
1 | 用户连续输入 "typescript"(每 50ms 一个字符) |
2. 解题思路
一句话:用一个闭包持有的定时器 ID,每次触发先 clearTimeout 再重新 setTimeout。这样只有「最后一次触发」的定时器能活到时间并执行回调。
要点:
timer存在闭包里,外界看不见,只在函数内部读写。- 执行回调时用
fn.apply(this, args),保证this指向调用方(比如事件绑定的 DOM),参数也原样透传。 immediate版本:若已立即执行过,就把timer设为一个哨兵值表示「冷却中」,静默期结束后置空以重新武装。cancel:清掉当前定时器并把状态复位。
- 时间复杂度:每次触发
O(1)。 - 空间:闭包一份。
3. TypeScript 实现
3.1 标准版(延迟执行,最常用)
1 | function debounce<T extends (...args: any[]) => void>( |
3.2 支持 immediate(首次立即执行)
1 | function debounceImmediate<T extends (...args: any[]) => void>( |
4. 面试延伸
- 防抖 vs 节流:防抖「等安静了再执行一次」,节流「固定频率最多执行一次」。二者常一起考,见下一篇 [节流]。
- 带返回值怎么办? 防抖天然会推迟结果,直接 return 拿不到。工程做法:返回 Promise,或用回调;能讲清这个取舍很加分。
- leading / trailing 配置:lodash 的
debounce有leading(是否先行执行)、maxWait(最长等待,防止永远不执行,此时退化为带节流的防抖)。 - React 中要配合
useMemo/useRef保存同一个 debounce 实例,否则每次渲染都新建、定时器互相打架。
难度:简单 | 前端手写题必考 | 事件频率控制基石