理解防抖

本文最后编辑于:2025年5月31日 晚上

为什么需要防抖

防抖主要应用在一些高频率触发的事件中,比如:scroll 滚动、input 输入、resize 缩放等,能显著降低性能开销。

什么是防抖

防抖的核心思想是:高频率触发的事件中,仅保留最后一次操作,之前的操作都会被取消。

举例来说,当你在页面中不断滚动,防抖函数会等待设定的一段时间,如果这段时间内没有再次触发滚动,才会执行处理逻辑;否则会重新计时,直到用户 “安静下来” 再执行操作。

防抖函数的一般形式如下:

1
2
3
4
5
6
7
8
9
10
function debounce(fn, delay) { // fn为传入的函数名
  let timer = null;  // 用来保存定时器的 ID
  return function(...args) {  // 返回一个闭包函数
    if (timer) clearTimeout(timer);  // 若已有定时器则清除它
    timer = setTimeout(() => {  // 设置一个新的定时器
      // 当延时结束时,执行传入的函数,并绑定当前的上下文和参数  
      fn.apply(this, args);
    }, delay);  // delay 是等待的时间,单位是毫秒 ms
  };
}

使用示例

假设我们有一个函数 handleScroll,它在页面滚动时处理一些操作,我们不希望它每次滚动都触发,因为这会增加不必要的性能消耗,我们希望它在滚动停止后执行,那么可以这样使用防抖函数

1
2
3
4
5
6
7
8
9
10
function handleScroll() {
    console.log("处理滚动事件中...");
  	// 实际逻辑略
}

// 使用防抖包装滚动事件处理函数,等待300毫秒
const debouncedHandleScroll = debounce(handleScroll, 300);

// 当页面滚动时
window.addEventListener('scroll', debouncedHandleScroll);

这样处理后,滚动事件停止 300ms 后才会触发 handleScroll,大大减少了触发频率。

理解防抖函数

每次触发 debounce 返回的函数时:

  1. 如果存在旧的定时器,则先清除;
  2. 然后设置一个新的定时器;
  3. 只有在用户停止触发事件一段时间后,才执行原始函数。

换句话说:

多次触发事件时,只有最后一次触发后的延迟时间结束后才会执行目标函数。

什么是 timer?

timer 是由 setTimeout 返回的定时器标识符,用来取消未完成的定时任务。

具体来说,setTimeout 的返回值是一个定时器的标识符 ID。而定时器的 ID 的类型因环境不同也有所差异:在浏览器环境中,setTimeout 的返回是一个数值类型的 ID,例如第一次调用可能返回 1,第二次为 2,依次递增;在 Node.js 环境中,setTimeout 返回值可能是一个定时器对象,但仍然可以通过 clearTimeout 消除。当然也可以将 timer 写作别的名称,只要合法都行,但为了直观还是建议写成 timer

为什么要每次清除旧定时器?

如果不清除旧的定时器,每次事件触发都会设置一个新的定时器,导致多个定时器同时生效,函数可能会被多次调用,完全违背了 “只执行最后一次” 的目的。

现在,假设每次开始都清除定时器,那么当前的滑动事件会将上一次的滑动事件定时清除,并为自己这一次的滑动事件进行定时,而这一次若为最后一次,函数将不再会被调用,也就不再会清除定时器,这一次的事件将会被正确的以设置好的时间定时进行处理。

电梯门的防抖类比

这种事件可以类比生活中的乘电梯事件:将电梯门看作事件处理函数,进入电梯的这一动作看作不希望的高频次事件。当人按下电梯按钮,电梯抵达并开门,开门的同时开始计时,每有一个人被红外检测到,人经过电梯门进入电梯之后,电梯门的计时就重新开始(清除上一次的计时),直到没有人进入(高频次事件停止,进入电梯的最后一个人为最后的事件),电梯门最终关闭(定时器时间到了),电梯执行上升或下降等约定好的操作(执行回调函数)。

即:

  1. 每当一个人进入电梯,门会重新计时关门;
  2. 如果不断有人进来,门就不会关;
  3. 直到没人再进来,计时器才不会被清除,门会在设定时间后关闭。

这个直到没人再进来的延迟,就是防抖的关键。

为什么不将 timer 写在外部?为什么返回一个闭包函数?

timer 在功能上看,当然可以作为全局变量定义在整个函数外部,但是这样一来如果别处也要使用 timer 就会显得不太合适,timer 会被污染,不便于函数的封装复用,并且还存在安全问题,将其私有化是一个好的选择。

此外闭包可以记录外部变量的状态(这主要依靠作用域链),这样能保证多次触发事件访问到的是同一个timer,保证多次事件触发能清除到前一个事件的定时,最终实现防抖。

这闭包到底是啥?

闭包(closure)是 JavaScript 中一个重要的概念,闭包是指一个函数可以访问它外部函数作用域中的变量,即使在外部函数已经执行结束后,它仍然可以通过引用这些变量。

什么意思呢?假设有这样一个函数

1
2
3
4
5
6
7
8
9
10
11
12
function createCounter() {
    let count = 0;
    return function() {
        count += 1;
        return count;
    };
}

const counter = createCounter();
console.log(counter());  // 输出 1
console.log(counter());  // 输出 2
console.log(counter());  // 输出 3

按理说一个函数 return 之后函数本身就已经 over 了,但是 counter 还是能够访问到 count 的值,并进行了累加。类似这样的一个返回的函数就是闭包。闭包一般是在函数嵌套的情况下形成的。当一个函数 A 返回另一个内部函数 B,并且 B 可以访问 A 函数内部定义的变量时,闭包就形成了。

...args 是什么?为什么?

在 JavaScript 中,...args 是一种称为 剩余参数(Rest Parameters) 的语法,它用于将函数的不定数量的参数收集到一个数组中。这样可以让函数接受任意数量的参数,并将它们组合在一起,便于操作和处理。

说到为什么要使用它,还得说说它的好处。假设有这样一个函数

1
2
3
4
5
6
7
function logArguments(...args) {
  console.log(args);  // args 是一个数组,包含了所有传入的参数
}

logArguments(1, 2, 3);   // 输出 [1, 2, 3]
logArguments("a", "b");  // 输出 ["a", "b"]
logArguments();          // 输出 []

不论传入多少参数,有还是没有,它都能够接收并存储在 args 数组中。

那为什么防抖中要用到呢?

一来是为了使得函数能够处理任意数量的参数,使其更通用;二来是为了能够正确地传递参数。

可别不以为意,有时候还真没多少人能正确地传递参数。

节流

⚠️防抖(debounce)和节流(throttle)经常被搞混:

  • 防抖:N 秒内事件不断触发,只执行最后一次(停止触发 N 秒后执行)
  • 节流:N 秒内只执行一次(无论触发多少次,都限制频率)

节流适用场景

  • 无限滚动分页加载
  • 页面滚动触发动画
  • 鼠标移动实时反馈等

防抖变种:立即执行(immediate

上述讨论的是 “非立即执行的防抖”,即:等事件不再触发后才执行一次

有时我们希望首次触发立即执行函数,后续触发不再响应,直到一段时间之后再允许触发,这就属于 “立即执行防抖”。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function debounce(fn, delay, immediate = false) {
  let timer = null;
  return function (...args) {
    const context = this;
    if (timer) clearTimeout(timer);
    if (immediate && !timer) {
      fn.apply(context, args);  // 首次立即执行
    }
    timer = setTimeout(() => {
      if (!immediate) fn.apply(context, args); // 非立即执行逻辑
      timer = null;
    }, delay);
  };
}

这种变种适用场景例如:

  • 用户首次输入时立即触发搜索建议
  • 防止按钮被连续点击:首次有效,接下来点击无效
  • 页面初始化后的 resize 初始化布局等

总结

类型执行时机典型场景
防抖 Debounce停止触发后一段时间执行输入框搜索、滚动监听、resize
节流 Throttle固定频率执行上拉加载、鼠标移动、FPS 控制等
立即执行防抖首次触发立即执行快速响应用户操作(防重复点击)

理解防抖
https://4rozen.github.io/archives/Vue/35829.html
作者
4rozeN
发布于
2023年7月6日
许可协议