Skip to content
On This Page

节流和防抖

1. 为什么需要节流和防抖

浏览器中的许多事件触发频率非常高:

  • scroll 事件:滚动时每秒触发数十次甚至上百次。
  • resize 事件:窗口大小改变时连续触发。
  • input 事件:用户在输入框中每敲击一个键都会触发。
  • mousemove 事件:鼠标移动时连续触发。

如果直接在这些事件的回调中执行复杂操作(如网络请求、DOM 操作、重渲染等),会导致:

  • 性能下降、页面卡顿。
  • 不必要的资源浪费(如发送大量重复请求)。
  • 逻辑错乱(例如搜索框实时提示,每次按键都请求接口)。

节流 和 防抖 就是用来限制函数执行频率的两种策略。

2. 防抖(Debounce)

2.1 原理

防抖:在事件被触发后,延迟 n 秒再执行回调。如果在 n 秒内事件再次被触发,则重新计时。

简单理解:“你尽管触发,我只在最后一次触发后等待一段时间才执行”

2.2 形象比喻

  • 电梯关门:电梯门即将关闭时,有人进来了,门会重新打开并等待一段时间,直到没有人进入才真正关闭。

2.3 实现

基础版(立即执行? 延迟执行?)

javascript
function debounce(func, delay, immediate = false) {
  let timerId;
  return function(...args) {
    const context = this;
    if (timerId) clearTimeout(timerId);
    if (immediate) {
      // 立即执行一次,然后等待 delay 内不再触发才能再次执行
      const callNow = !timerId;
      timerId = setTimeout(() => {
        timerId = null;
      }, delay);
      if (callNow) func.apply(context, args);
    } else {
      timerId = setTimeout(() => {
        func.apply(context, args);
      }, delay);
    }
  };
}

常见简化版(延迟执行)

javascript
function debounce(func, delay) {
  let timer;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => func.apply(this, args), delay);
  };
}

2.4 应用场景

  • 搜索框实时搜索(用户停止输入 500ms 后才请求)
  • 窗口大小调整(resize)重新计算布局(调整结束后再计算)
  • 表单验证(用户停止输入后验证)
  • 保存草稿(用户停止编辑后自动保存)

3. 节流(Throttle)

3.1 原理

节流:在事件持续触发时,保证在一定时间间隔内只执行一次回调。

简单理解:“不管你触发多快,我每隔 n 秒才执行一次”

3.2 形象比喻

  • 机关枪射速限制:每秒最多射出 10 发子弹,即使你扣住扳机不放,也不会超过这个频率。
  • 游戏技能冷却:技能释放后需要等待几秒才能再次释放。

3.3 实现

节流有两种常见实现方式:定时器(延迟执行,冷却结束后执行)和 时间戳(立即执行,然后冷却)。

方式一:定时器(第一次延迟执行,最后一次会执行)

javascript
function throttle(func, delay) {
  let timer = null;
  return function(...args) {
    if (!timer) {
      timer = setTimeout(() => {
        func.apply(this, args);
        timer = null;
      }, delay);
    }
  };
}

方式二:时间戳(第一次立即执行,最后一次可能被忽略)

javascript
function throttle(func, delay) {
  let lastTime = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastTime >= delay) {
      func.apply(this, args);
      lastTime = now;
    }
  };
}

方式三:组合版(支持首尾都执行)

javascript
function throttle(func, delay) {
  let lastTime = 0;
  let timer = null;
  return function(...args) {
    const now = Date.now();
    const remaining = delay - (now - lastTime);
    if (remaining <= 0) {
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      func.apply(this, args);
      lastTime = now;
    } else if (!timer) {
      timer = setTimeout(() => {
        func.apply(this, args);
        lastTime = Date.now();
        timer = null;
      }, remaining);
    }
  };
}

3.4 应用场景

  • 滚动加载(监听滚动到底部加载更多,限制频率)
  • 页面滚动时的动画或高亮(每 100ms 计算一次)
  • 按钮重复点击(防止短时间内多次提交)
  • 游戏中的射击频率限制
  • 浏览器 resize 事件(需要持续响应但不想太密集)

4. 防抖 vs 节流 对比表

特性防抖(Debounce)节流(Throttle)
执行时机连续触发停止后,等待一段时间执行固定时间间隔内执行一次
频率控制将多次触发合并为最后一次稀释为每隔 n 秒执行一次
适合场景更关心“最后的结果”更关心“过程的频率控制”
典型例子搜索输入、resize 后计算游戏射击、滚动加载、拖拽位置上报
是否可能丢失执行可能丢失中间的执行(只执行最后一次)不会丢失(在周期内执行一次,但可能会跳过一些触发)