防抖
基础知识
什么是防抖 (Debounce)? 防抖是一种性能优化策略,用于限制函数在特定时间内的执行频率。它的核心思想是:将连续的、密集的函数调用合并为一次,只在最后一次调用发生后的指定时间段内没有新的调用时,才真正执行该函数。
生活中的类比 想象一下电梯关门的场景:
- 当有人走进电梯,门准备关闭的倒计时(比如 5 秒)开始。
- 如果在倒计时结束前,又有人按了开门按钮或走了进来,倒计时会重置,重新开始 5 秒倒计时。
- 只有当倒计时完整地走完,中间没有任何人再进来,门才会最终关闭。 在这个例子中,“关门”这个动作就被“防抖”了。
应用场景 防抖非常适用于那些由用户连续操作触发,但我们只关心其最终结果的场景:
- 搜索框输入建议: 用户在输入框中连续输入时,我们不希望每输入一个字符就发送一次 API 请求,而是希望在用户停止输入一小段时间后再发送。
- 窗口大小调整 (
resize
事件): 当用户拖动调整浏览器窗口大小时,resize
事件会高频触发。我们不希望在过程中不断重新计算布局,而是在用户停止拖动后,再执行一次最终的布局计算。 - 按钮重复点击: 防止用户因网络延迟等原因,快速重复点击提交按钮,导致表单被提交多次。
核心思路
防抖的核心实现机制,是巧妙地利用闭包和定时器 (setTimeout
)。
高阶函数: 我们需要创建一个高阶函数
debounce
。这个函数接收两个参数:要进行防抖处理的原始函数func
,以及一个延迟时间delay
。它会返回一个新函数。闭包与定时器 ID: 在
debounce
函数内部,但在返回的新函数外部,我们需要一个变量(例如timer
)来存储setTimeout
返回的定时器 ID。由于闭包的特性,这个timer
变量会在多次调用返回的新函数之间保持存在和共享。返回的新函数(“防抖函数”)的逻辑:
- 当这个新函数被触发时,它要做的第一件事就是清除之前设置的任何定时器(
clearTimeout(timer)
)。这就是“重置倒计时”的核心步骤。 - 然后,它会设置一个新的定时器(
setTimeout
)。 - 这个新的定时器会在指定的
delay
毫秒后,真正执行原始函数func
。
- 当这个新函数被触发时,它要做的第一件事就是清除之前设置的任何定时器(
this
指向和参数传递: 原始函数func
在执行时,可能需要正确的this
上下文和参数。因此,在我们的防抖函数内部,需要捕获调用时的this
和arguments
,并在最终执行func
时通过apply
或call
传递给它。
代码实现
下面我们提供一个基础版和一个功能更全面的进阶版实现。
版本一:基础实现
/**
* 防抖函数(基础版)
* @param {Function} func - 需要防抖的函数
* @param {number} delay - 延迟时间,单位毫秒
* @returns {Function} - 返回一个新的防抖函数
*/
function debounce(func, delay) {
let timer = null; // 利用闭包保存定时器 ID
return function (...args) {
const context = this; // 保存调用时的 this 上下文
// 如果已有定时器,则清除它
if (timer) {
clearTimeout(timer);
}
// 设置新的定时器
timer = setTimeout(() => {
func.apply(context, args);
}, delay);
};
}
版本二:进阶实现(支持立即执行和取消)
/**
* 防抖函数(进阶版)
* @param {Function} func - 需要防抖的函数
* @param {number} delay - 延迟时间,单位毫秒
* @param {boolean} [immediate=false] - 是否在第一次触发时立即执行
* @returns {Function} - 返回一个新的防抖函数,并附带 cancel 方法
*/
function debounce(func, delay, immediate = false) {
let timer = null;
let result;
const debounced = function (...args) {
const context = this;
// 清除之前的定时器,以便重新计时
if (timer) {
clearTimeout(timer);
}
if (immediate) {
// 1. "立即执行" 逻辑
const callNow = !timer; // 如果 timer 为 null,说明是第一次调用
timer = setTimeout(() => {
timer = null; // delay 时间后,清空 timer,允许下一次“立即执行”
}, delay);
if (callNow) {
result = func.apply(context, args);
}
} else {
// 2. "非立即执行" 逻辑
timer = setTimeout(() => {
result = func.apply(context, args);
}, delay);
}
// 对于立即执行的情况,返回上一次的结果
return result;
};
// 3. 添加 cancel 方法
debounced.cancel = function () {
clearTimeout(timer);
timer = null;
};
return debounced;
}
代码解析
基础版解析
timer 变量核心作用:
let timer = null
是防抖的核心机制,通过闭包被debounce
函数返回的匿名函数共享。- 该变量如同「闹钟」,记录下一次函数执行的计划,确保在指定延迟内只执行一次函数。
clearTimeout 重置机制:
- 每次防抖函数被调用时,先执行
clearTimeout(timer)
取消上一个未执行的「闹钟」。 - 此操作实现「重置」效果,保证只有最后一次调用的延迟结束后才会执行函数。
- 每次防抖函数被调用时,先执行
上下文与参数处理:
const context = this
保存调用时的上下文(如 DOM 元素)。...args
收集所有传入参数(如事件对象event
),确保原始函数调用时参数一致。
函数执行与上下文绑定:
func.apply(context, args)
在setTimeout
回调中执行原始函数。- 通过
apply
确保函数执行时的this
上下文和参数与最后一次调用防抖函数时一致。
进阶版解析
immediate 立即执行逻辑:
- 当
immediate: true
时,函数在事件触发时立即执行而非延迟结束后执行。 const callNow = !timer
巧妙判断是否处于冷却期(timer为null
表示可立即执行)。- 若
callNow
为true
,立即执行func
,并设置定时器在延迟后重置timer
。 - 定时器此时仅用于开启「冷却期」,而非执行函数,确保冷却期内多次调用不会重复执行。
- 当
result 变量返回值处理:
- 用于存储
func
的返回值,仅在immediate
模式下有效(函数同步执行)。 - 非
immediate
模式下函数异步执行,直接返回undefined
,是防抖的固有特性。
- 用于存储
debounced.cancel 方法:
- 通过给返回的
debounced
函数添加cancel
方法暴露取消能力。 - 调用
debounced.cancel()
会清除定时器并重置状态,常用于组件卸载时取消待处理任务(如 API 请求)。
- 通过给返回的