防抖函数依赖闭包中 let 声明的 timer 引用实现状态同步:每次触发先 cleartimeout(timer),再 settimeout 赋新值;需在立即执行后手动置 null,组件卸载时须调用 cancel 清理。

防抖函数的核心在于:每次触发时清除上一次的定时器,再设置新的定时器;而这个“清除”动作依赖闭包中保存的 timer 引用。关键不是 timer 本身多复杂,而是它在多次调用间如何被正确读取、覆盖和清空——这就是状态同步的本质。
timer 只是一个引用,不是状态本身
闭包捕获的是对定时器 ID(如 number)的引用,而非定时器对象。浏览器 API(setTimeout)返回的 ID 是一个数字,仅用于后续 clearTimeout 查找并终止对应任务。闭包里的 timer 变量只是存这个数字的“记事本”,它的值变没变,直接决定下一次是否能清掉旧任务。
- 首次调用 →
timer = setTimeout(...)→timer变成一个有效数字(如 5) - 第二次调用 → 先执行
clearTimeout(timer)(此时传入 5,成功清除)→ 再赋新值timer = setTimeout(...) - 若中间没触发,定时器自然执行,但
timer还是那个数字;执行完后不自动归零,需手动重置(可选)
立即执行 + 延迟重置的常见陷阱
带 immediate 选项的防抖(首次立即执行,后续等待)中,timer 的同步更易出错。典型错误是:立即执行后忘记把 timer 设为 null 或 undefined,导致下一次触发时仍尝试 clearTimeout(undefined)(无害但冗余),或更糟——漏清上一轮延迟执行的定时器。
- 推荐模式:无论是否立即执行,只要进入函数体,第一件事就是
if (timer) clearTimeout(timer) - 立即执行分支结束后,应显式
timer = null;延迟执行分支则由setTimeout自动赋新值 - 不重置
timer会导致“假清除”:clearTimeout(null)不报错但无效,旧定时器可能仍在运行
使用 let 声明保证块级独立性
如果防抖函数被多次调用生成不同实例(比如给多个按钮分别绑定),每个实例必须有自己独立的 timer。用 let timer 在闭包内声明,可确保每次调用 debounce(fn, delay) 都创建新的词法环境,互不干扰。
- 错误写法:
var timer放在闭包外 → 所有实例共享同一个timer,互相覆盖 - 正确写法:
return function() { let timer; ... }或更常见地,在 debounce 工厂函数内部用let timer声明 - 箭头函数无法创建新作用域,所以 timer 必须定义在返回的函数体内或其外层闭包中
清理时机:组件卸载或手动销毁时别忘清 timer
在 React、Vue 等框架中,防抖函数常作为事件处理器存在。若组件卸载时定时器还未执行,timer 仍持有对已销毁上下文的引用,可能引发内存泄漏或报错(如 Cannot set property of null)。
- 暴露一个
cancel方法:在闭包中返回一个函数,内部执行clearTimeout(timer); timer = null; - React 中可在
useEffect清理函数里调用该cancel;Vue 中在beforeUnmount调用 - 注意:
cancel不能只清 timer,还要确保后续调用不会因闭包中变量失效而异常(例如访问已卸载组件的 state)











