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

在前端开发中,我们经常会遇到一些高频率触发的事件,比如窗口的 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 进行链式调用。


相关推荐
爱编程的喵1 分钟前
JavaScript闭包实战:从类封装到防抖函数的深度解析
前端·javascript
前端Hardy7 分钟前
8个你必须掌握的「Vue」实用技巧
前端·javascript·vue.js
星月日10 分钟前
深拷贝还在用lodash吗?来试试原装的structuredClone()吧!
前端·javascript
爱学习的茄子11 分钟前
JavaScript闭包实战:解析节流函数的精妙实现 🚀
前端·javascript·面试
今夜星辉灿烂24 分钟前
nestjs微服务-系列4
javascript·后端
吉吉安26 分钟前
两张图片对比clip功能
javascript·css·css3
布兰妮甜36 分钟前
开发在线商店:基于Vue2+ElementUI的电商平台前端实践
前端·javascript·elementui·vue
Jinxiansen021140 分钟前
Vue 3 中父子组件双向绑定的 4 种方式
javascript·vue.js·ecmascript
木依44 分钟前
Vue3 Element plus table有fixed列时错行
javascript·vue.js·elementui
Hilaku2 小时前
我为什么放弃了“大厂梦”,去了一家“小公司”?
前端·javascript·面试