我对防抖(Debounce)的一点理解与实践
这篇文章主要是我在项目中使用防抖过程中的一些总结,只代表个人理解,如果有不严谨或可以优化的地方,欢迎指出和讨论。
一、防抖的概念
防抖(Debounce) ,简单来说就是:
在短时间内多次触发同一个函数时,只让它在“合适的时机”执行一次。
常见的两种形式:
- 尾触发:停止触发一段时间后才执行
- 立即执行:第一次触发立刻执行,随后一段时间内不再执行
防抖本身并不复杂,真正复杂的地方在于:什么时候该用哪一种,以及实现细节是否可靠。
二、为什么要做防抖(重点)
在实际项目中,高频触发几乎无处不在:
- 用户快速点击按钮
- 表单多次提交
- 输入框实时搜索
如果不加控制,往往会带来一些问题:
- 接口被重复调用
- 产生重复副作用(多次提交、多次弹窗)
- 状态错乱,难以维护
防抖解决的核心问题是:
函数触发频率过高,而这些触发中,只有一部分是真正“有意义”的。
通过防抖,我们可以在函数入口处统一控制执行频率,而不是在函数内部到处加判断。
2.1 除了防抖,还有其它方案吗?(简单带过)
实际开发中,也经常能看到一些方案:
-
loading 状态控制:
-
页面多个按钮,loading按钮过多,然后二次封装按钮组件:
接下来先按照我个人的理解,来说一下还是防抖。
三、基础版本防抖实现
3.1 最基础的防抖写法
function debounce(func, wait = 200) {
let timeout = null
return function (...args) {
clearTimeout(timeout)
timeout = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}
这个版本属于最经典的尾触发防抖:
- 多次触发只会执行最后一次
3.2 普通函数与箭头函数的区别
防抖实现中经常会看到两种写法:
const content = this
setTimeout(function () {
func.apply(content, args)
}, wait)
以及:
setTimeout(() => {
func.apply(this, args)
}, wait)
这两种写法的核心区别在于 this 的绑定机制不同:
- 普通函数的
this是运行时动态绑定的,由函数的调用方式决定,在setTimeout等场景中容易发生this丢失。 - 箭头函数不会创建自己的
this,它的this在定义时就已经确定,始终指向外层作用域的this,因此非常适合用于定时器和回调函数中。
因此在防抖中,如果使用普通函数,往往需要额外保存 this;
而使用箭头函数,可以让代码更简洁。然后具体的情况需要具体分析
这里不展开细说 this 的规则。
而且里面还涉及到了apply,call,bind等知识
四、立即执行版防抖
4.1 为什么需要立即执行版
普通防抖有一个明显特点:
- 第一次触发不会立即执行
在一些场景下,这并不是我们想要的行为,例如:
- 提交按钮
- 登录、支付等关键操作
这类场景下,更合理的预期是:
第一次点击立刻执行,但在短时间内禁止再次触发。
这就是立即执行版防抖存在的意义。
4.2 立即执行版完整实现
function debounce(func, wait = 200, immediate = false) {
let timeout = null
return function (...args) {
// 是否需要立即执行(第一次触发)
const callNow = immediate && !timeout
// 清除之前的定时器
if (timeout) clearTimeout(timeout)
// 设置新的定时器,用于冷却期结束
timeout = setTimeout(() => {
// 冷却结束,重置状态
timeout = null
// 非立即执行模式,走尾触发
if (!immediate) {
func.apply(this, args)
}
}, wait)
// 立即执行(只会执行一次)
if (callNow) {
func.apply(this, args)
}
}
}
4.3 这一版的核心思路
在这个实现中:
timeout不只是一个定时器- 它同时承担了 “是否处于冷却期” 的状态标记
const callNow = immediate && !timeout
!timeout表示当前不在冷却期- 只允许第一次触发立即执行
当定时器结束后:
timeout = null
表示冷却期结束,允许下一次立即执行。
五、结合源码理解实现逻辑
在理解了立即执行版防抖的实现后,再回看 Underscore.js 的 _.debounce 源码,其实可以发现:核心思想完全一致,只是写法更偏工程化。
_.debounce = function(func, wait, immediate) {
var timeout, result;
var later = function(context, args) {
timeout = null;
if (args) result = func.apply(context, args);
};
var debounced = function(...args) {
if (timeout) clearTimeout(timeout);
if (immediate) {
var callNow = !timeout;
timeout = setTimeout(later, wait);
if (callNow) result = func.apply(this, args);
} else {
timeout = setTimeout(() => later(this, args), wait);
}
return result;
};
return debounced;
};
timeout 是防抖的核心状态
timeout 不只是定时器 ID,更是是否处于冷却期的状态标识:
timeout === null:不在冷却期timeout !== null:正在防抖中
立即执行模式正是通过 !timeout 来判断“是否第一次触发”。
为什么定时器里要 timeout = null
timeout = null;
这一步表示冷却期结束,为下一次立即执行创造条件。
如果不重置,immediate 只会生效一次。
立即执行的关键逻辑
var callNow = !timeout;
timeout = setTimeout(later, wait);
if (callNow) func.apply(this, args);
这三行完成了三件事:
- 判断是否第一次触发
- 立刻进入冷却期
- 只在第一次触发时立即执行
后续触发只会刷新定时器,不会重复执行。
为什么源码不用 this,而是传 context
later 是普通函数,this 不可靠。
因此 Underscore 选择 显式传递 context,保证 this 指向稳定,这是典型的库级写法。
核心结论
防抖并不依赖复杂 API,本质只有两点:
- 定时器
- 状态控制(是否处于冷却期)
立即执行与否,本质区别只是:
函数是在冷却期开始时执行,还是在冷却期结束时执行。
总结
防抖本身并不难,真正容易出问题的是:
- 使用场景选错
- this 指向理解不清
- 状态与执行职责混在一起
这篇文章更多是我个人在项目中的一些理解与总结,
如果你在实践中有不同的经验或看法,也非常欢迎交流。
752

被折叠的 条评论
为什么被折叠?



