高频触发?不存在的!防抖让你的交互从 “狂轰滥炸” 变 “丝滑躺平”

79 阅读8分钟

在前端开发中,我们经常会遇到一些高频触发的事件,比如按钮点击、输入框输入、窗口 resize 等。如果不对这些事件进行处理,频繁的触发会导致大量不必要的函数执行,不仅浪费资源,还可能影响用户体验。今天我们就来深入探讨一种解决这类问题的重要技术 —— 防抖(Debounce)

为什么需要防抖?

先来看两个常见的业务场景:

  1. 表单提交页面:用户输完内容点击提交按钮后,由于网络延迟没有立即得到响应,有些用户可能会多次点击提交按钮。这会导致多次请求发送到服务器,造成高并发,增加服务器压力,甚至可能出现重复提交的问题。

  2. 搜索框输入:在实现实时搜索功能时,如果不做任何处理,用户每输入一个字符就会触发一次请求。假设用户输入一个 10 个字符的关键词,就会发送 10 次请求,而实际上我们只需要在用户停止输入一段时间后发送一次请求即可。

防抖技术就是为了解决这类问题而存在的。防抖的核心思想是:在规定的时间内,没有新的事件触发,才执行相应的函数。如果在规定时间内又有新的事件触发,则重新计算等待时间。

防抖函数的实现与解析

下面我们通过一个具体的示例来理解防抖函数的实现,这是一个包含防抖功能的 HTML 文件(debounce.html):

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>防抖核心示例</title>
</head>
<body>
  <!-- 测试用提交按钮:高频点击触发源 -->
  <button id="btn">提交</button>

  <script>
    // 步骤1:获取按钮DOM元素
    let btn = document.getElementById('btn')

    // 步骤2:定义核心业务逻辑(实际要执行的操作)
    function handle(e) {
      console.log('提交成功!', '事件信息:', e, '触发元素:', this);
      // 真实场景替换为:接口请求/表单提交/数据处理等
    }

    // 步骤3:绑定点击事件(关键:用防抖函数包装业务逻辑)
    // 注意:不是直接传handle,而是传debounce(handle, 1000)的返回值
    btn.addEventListener('click', debounce(handle, 1000))

    // 步骤4:实现核心防抖函数(高阶函数+闭包)
    function debounce(fn, wait) {
      // 定时器ID,闭包特性使其持续存在
      var timer;
      // 返回实际触发的事件处理函数
      return function(...arg) {//解构
        // 清除上一次定时器,取消之前的执行计划
        clearTimeout(timer);
        // 重新设置定时器,开始新一轮倒计时
        timer = setTimeout(() => {
          // 绑定this和传递参数,确保业务函数执行上下文正确
          fn.apply(this, arg);
        }, wait); // wait=1000ms:两次点击间隔<1秒则重新计时
      }
    }
  </script>
</body>
</html>

关键技术点拆解(必懂!)

1. 为什么是 “高阶函数”?

debounce 函数接收两个参数:fn(业务函数)和 wait(等待时间),最终返回一个新函数—— 这是高阶函数的典型特征。正因为返回了新函数,我们才能在里面包装 “清除定时器 + 重新计时” 的防抖逻辑,而不污染原始业务函数 handle

2. 闭包的核心作用(重中之重)

var timer 是定义在 debounce 内部的变量,但它没有随着 debounce 执行完毕而销毁 —— 因为返回的匿名函数一直在引用它,这就是闭包

  • 没有闭包:每次点击按钮都会创建一个新的 timer,无法跟踪上一次的定时器状态,防抖就失效了;

  • 有了闭包:所有点击事件共享同一个 timer,才能实现 “清除上一次定时器” 的核心逻辑。

3. 为什么要用 apply 绑定 this 和参数?fn.apply(this, arg);

如果直接写 fn(),会出现两个问题:

  • this 指向错误:handle 里的 this 会变成 window(非严格模式),而不是触发事件的按钮;

  • 参数丢失:点击事件的 e 对象(包含点击位置、触发元素等信息)无法传递给 handle

fn.apply(this, arg) 完美解决:

  • this 绑定当前触发事件的 DOM 元素(按钮),与直接绑定 handle 的行为一致;

  • arg 传递收集到的参数(如 e),保证业务逻辑能获取完整的事件信息。

4. 深入理解 this 在代码中的完整流向(重点补充!)

很多同学搞不懂 handle 里的 this 为什么能指向按钮,这里用 “追踪法” 一步步拆解 this 的指向逻辑:

第一步:debounce 函数执行时的 this

当执行 debounce(handle, 1000) 时,debounce直接调用的(没有通过某个对象调用),所以此时 debounce 内部的 this 指向 window(非严格模式)—— 但注意:我们关心的不是这里的 this,而是业务函数 handle 里的 this

第二步:返回的匿名函数执行时的 this

debounce 函数返回的匿名函数,最终被绑定到按钮的 click 事件上(btn.addEventListener('click', 匿名函数))。

根据 DOM 事件绑定的规则:事件处理函数执行时,this 会自动指向触发该事件的 DOM 元素

所以当用户点击按钮时,匿名函数执行,此时匿名函数内部的 this 指向 btn(提交按钮)。

第三步:handle 函数执行时的 this

匿名函数内部通过 fn.apply(this, arg) 执行 handle 函数:

  • apply 方法的第一个参数,就是指定 fn(即 handle)执行时的 this 指向;

  • 这里传入的 this 是匿名函数的 this(也就是 btn),所以 handle 里的 this 最终指向按钮。(重点!!!)

反例:如果不用 apply 会怎样?

如果直接写 fn()(不绑定 this),handle 会作为普通函数执行,此时 this 指向 window(非严格模式),控制台会打印 触发元素:Window,而不是我们需要的按钮元素 —— 这会导致后续如果想通过 this 操作按钮(比如修改按钮文字为 “提交中...”)时,代码会失效。

执行流程演示(1 秒等待时间)

假设用户 1 秒内连点 3 次按钮,看看防抖是怎么 “拿捏” 的:

时间点用户操作内部执行逻辑
0ms第一次点击1. timer 初始为 undefined,clearTimeout 无效果;2. 设置定时器,计划 1000ms 后执行 handle
500ms第二次点击1. 清除 0ms 时的定时器(取消第一次执行);2. 重新设置定时器,计划 1500ms 后执行
1200ms第三次点击1. 清除 500ms 时的定时器(取消第二次执行);2. 重新设置定时器,计划 2200ms 后执行
2200ms无操作定时器到期,执行 handle,控制台打印 “提交成功”,此时 handle 里的 this 指向 btn 按钮

最终结果:3 次点击只执行 1 次业务逻辑,且 this 指向正确,完美避免重复提交!

注意事项(避坑指南)

  1. 事件绑定的是 “返回的新函数”
  • 错误写法:btn.addEventListener('click', debounce(handle, 1000)())(加了括号会立即执行,防抖失效);

  • 正确写法:btn.addEventListener('click', debounce(handle, 1000))(只传函数引用,点击时才执行)。

  1. 等待时间 wait 的选择
  • 按钮提交:1000ms 左右(给用户反应时间,避免误点);

  • 搜索框输入:300-500ms(平衡响应速度和请求次数);

  • 窗口 resize / 滚动:200-300ms(减少布局计算次数)。

  1. 参数传递不丢失
  • ...arg 收集参数(ES6 剩余参数语法),兼容多参数场景(比如除了 e 还需要传递其他自定义参数)。
  1. this 指向的特殊场景
  • 如果在 Vue/React 等框架中使用,事件处理函数的 this 可能被框架绑定为组件实例,此时需通过 箭头函数bind 手动调整,确保 this 指向正确;

  • 严格模式下,普通函数执行时 thisundefined,而非 window,更能凸显 apply 绑定 this 的必要性。

实际应用场景扩展

除了按钮提交,防抖还能解决这些问题:

  • 搜索框实时联想:用户输入停顿后再发请求,减少无效接口调用;

  • 窗口 resize:浏览器窗口调整时,避免频繁执行布局调整函数;

  • 滚动加载:页面滚动时,避免高频触发 “加载更多” 请求;

  • 拖拽事件:拖拽元素时,减少 mousemove 事件的高频计算。

总结

防抖的核心逻辑其实很简单:“高频触发时,只执行最后一次”,而实现这一切的关键是「高阶函数 + 闭包 + 定时器」的组合。

其中 this 的指向是容易踩坑的点,记住核心结论:通过 apply 将事件处理函数(匿名函数)的 this 传递给业务函数 handle,确保 handle 能正确访问触发事件的 DOM 元素