防抖与节流:前端开发中的高频事件优化

在前端开发中,我们经常会遇到一些高频率触发的事件,比如窗口的 resize、页面的 scroll、输入框的 input、按钮的 click 等。如果不加以控制,这些事件会在极短的时间内被多次触发,导致性能问题,甚至引发页面卡顿。为了解决这个问题,前端开发中常用两种优化手段:防抖(Debounce)节流(Throttle)


一、什么是防抖(Debounce)?

防抖 的核心思想是:在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。也就是说,只有最后一次事件触发后,经过指定时间才会执行回调。

就好比你在敲门,只有在你停止敲门一段时间后,屋里的人才会来开门。如果你一直敲,屋里的人就一直等,直到你停下来。

常用于:

  • 搜索框实时联想(用户输入时,防止每输入一个字符就发送一次请求)
  • 窗口大小调整(resize)结束后再执行计算
  • 表单验证(用户停止输入后再校验)

实现方式

javascript 复制代码
function debounce(fn, delay) {
  // 用于保存定时器的引用
  let timer = null;
  // 返回一个新的函数,替代原有的事件回调
  return function (...args) {
    // 每次触发时,先清除上一次的定时器
    clearTimeout(timer);
    // 重新设置定时器,delay毫秒后执行fn
    timer = setTimeout(() => {
      fn.apply(this, args); // 保证this指向和参数不变
    }, delay);
  };
}

使用样例:

js 复制代码
window.addEventListener('resize', debounce(() => {
  console.log('窗口大小调整结束');
}, 500));

这里的意思是:只有用户停止调整窗口 500ms 后,才会执行回调。如果在 500ms 内又触发了 resize,计时会重新开始。



二、什么是节流(Throttle)?

节流 的核心思想是:规定一个单位时间,在这个单位时间内,只能有一次事件处理函数被执行。即使在这个时间段内事件被多次触发,也只会执行一次。

假设你在马路上拍照,每隔 1 秒钟只能拍一张,无论你多快按快门,1 秒内只能拍一次。

常用于:

  • 页面滚动加载(scroll 事件,防止频繁触发)
  • 按钮防止多次点击提交
  • 游戏中的主循环(requestAnimationFrame)

实现方式

javascript 复制代码
function throttle(fn, delay) {
  // 记录上一次执行回调的时间
  let last = 0;
  // 返回一个新的函数,替代原有的事件回调
  return function (...args) {
    const now = Date.now();
    // 如果距离上次执行的时间超过了delay,则执行回调
    if (now - last > delay) {
      last = now;
      fn.apply(this, args); // 保证this指向和参数不变
    }
  };
}

使用样例:

javascript 复制代码
window.addEventListener('scroll', throttle(() => {
  console.log('页面正在滚动');
}, 200));

这里的意思是:无论 scroll 事件多频繁触发,回调函数最多每 200ms 执行一次。


三、防抖与节流的区别与选择

特性 防抖(Debounce) 节流(Throttle)
执行时机 停止触发后执行一次 间隔一段时间执行一次
适用场景 输入框、表单、resize等 scroll、mousemove等高频事件
目的 减少事件触发次数 限制事件触发频率

选取思路:

如果希望事件只在停止触发后执行一次,用防抖。

如果希望事件在一段时间内只执行一次,用节流。


四、防抖与节流的变种实现

在实际开发中,防抖和节流的需求往往比"只执行最后一次"或"每隔一段时间执行一次"更复杂。下面我结合自己的经验,深入讲讲常见的变种和实际应用细节。

4.1 防抖的立即执行与非立即执行

4.1.1 非立即执行(常规防抖)

这种方式就是前面讲的:事件触发后,只有在指定时间内不再触发,回调才会执行。适合"只关心用户停止操作后"的场景,比如输入框搜索。

4.1.2 立即执行版防抖

有时候,我希望用户第一次操作就立即响应,但后续在冷却期内不再触发。比如按钮防重复点击,第一次点击立刻响应,之后一段时间内无论怎么点都无效。

实现思路:

  • 第一次触发时立即执行回调,并设置定时器;
  • 冷却期内再次触发只会重置定时器,不会再次执行回调;
  • 冷却期结束后,允许下次立即执行。

代码实现与注释:

javascript 复制代码
function debounceImmediate(fn, delay) {
  let timer = null;
  return function (...args) {
    // 如果没有定时器,说明是第一次触发,立即执行
    if (!timer) {
      fn.apply(this, args);
    }
    // 每次触发都重置定时器
    clearTimeout(timer);
    timer = setTimeout(() => {
      timer = null; // 冷却期结束,允许下次立即执行
    }, delay);
  };
}

应用场景举例:

  • 防止表单重复提交
  • 按钮点击防抖

4.2 节流的两种实现方式

4.2.1 时间戳版节流

原理回顾:

每次事件触发时,判断距离上一次执行回调的时间是否超过了设定的间隔,如果超过就执行,否则不执行。

优点:

  • 首次触发会立即执行
  • 适合需要"立刻响应"的场景

4.2.2 定时器版节流

原理讲解:

  • 第一次触发时设置定时器,定时器到点后执行回调并清空定时器;
  • 在定时器期间内再次触发,不会重复设置定时器。

代码实现与注释:

javascript 复制代码
function throttleTimer(fn, delay) {
  let timer = null;
  return function (...args) {
    // 如果定时器不存在,说明可以执行
    if (!timer) {
      timer = setTimeout(() => {
        fn.apply(this, args);
        timer = null; // 执行完毕后清空定时器
      }, delay);
    }
  };
}

优点:

  • 最后一次触发一定会被执行
  • 适合"只关心最后一次操作"的场景

4.2.3 时间戳+定时器混合版节流

有时候我希望既能立即执行一次,也能保证最后一次操作被执行,这时可以结合时间戳和定时器:

实现思路:

  • 事件首次触发时立即执行
  • 之后每隔 delay 执行一次
  • 最后一次操作后,如果还有残留的事件,保证它也会被执行

代码实现与注释:

javascript 复制代码
function throttleHybrid(fn, delay) {
  let last = 0;
  let timer = null;
  return function (...args) {
    const now = Date.now();
    if (now - last > delay) {
      // 距离上次执行已超过delay,立即执行
      last = now;
      fn.apply(this, args);
    } else {
      // 否则设置定时器,保证最后一次也能被执行
      clearTimeout(timer);
      timer = setTimeout(() => {
        last = Date.now();
        fn.apply(this, args);
      }, delay - (now - last));
    }
  };
}

应用场景举例:

  • 页面滚动监听,既要响应首次滚动,也要在用户停止滚动后再处理一次

4.3 防抖与节流的参数扩展

在实际项目中,我们可能还需要给防抖和节流函数加上一些参数,比如:

  • 是否立即执行
  • 是否在冷却期结束后再执行一次
  • 最大等待时间(maxWait)

这样可以让函数更灵活,适应更多场景。


4.4 防抖与节流的注意事项

  1. this指向和参数传递

    在实现时要注意用 fn.apply(this, args) 保证回调里的 this 和参数不变。

  2. 取消功能

    有时候需要在某些场景下手动取消防抖/节流,可以在返回的函数上挂载一个 cancel 方法,清除定时器。

  3. 与Promise结合

    如果回调是异步操作,可以结合 Promise 进行链式调用。


相关推荐
jackyChan1 分钟前
ES6 Proxy 性能问题,你真知道吗?🚨
前端·javascript
我命由我1234537 分钟前
Vue 开发问题:Missing required prop: “value“
开发语言·前端·javascript·vue.js·前端框架·ecmascript·js
16年上任的CTO37 分钟前
一文讲清楚React中的key值作用与原理
前端·javascript·react.js·react key
阳火锅1 小时前
在生产环境下,你真的有考虑到使用数组方法的健壮性吗?
前端·javascript·面试
孤月葬花魂1 小时前
JavaScript 中的 Promise API 全面解析
前端·javascript
几道之旅1 小时前
Electron 应用打包全指南
前端·javascript·electron
shushushu1 小时前
Web如何自动播放音视频
前端·javascript
前端进阶者1 小时前
天地图InfoWindow插入React自定义组件
前端·javascript
扶我起来还能学_1 小时前
uniapp Android&iOS 定位权限检查
android·javascript·ios·前端框架·uni-app
爱学习的茄子1 小时前
JavaScript闭包实战:防抖的优雅实现
前端·javascript·面试