防抖与节流:高频事件性能优化的原理与手写实现

为什么需要防抖与节流?

在 Web 开发中,我们经常会遇到一些高频触发的事件,比如:

  • 窗口滚动(scroll):用户滚动页面时,事件每秒可能触发数十次甚至上百次。
  • 窗口大小调整(resize):拖动浏览器窗口时,事件持续高频触发。
  • 输入框输入(input):用户每敲一个字符都会触发一次。
  • 鼠标移动(mousemove):鼠标在页面上移动时,事件触发频率极高。

如果每次事件触发都执行复杂的逻辑(比如发送网络请求、操作 DOM、进行大量计算),页面性能会急剧下降,甚至出现卡顿、掉帧等体验问题。

防抖(Debounce)节流(Throttle) 就是解决这类问题的两种核心优化手段。

一、防抖(Debounce)

1.1 原理

防抖的核心思想是:在事件被触发后,延迟 n 秒再执行回调。如果在这 n 秒内事件又被触发,则重新计时。

简单来说,就是“等你停下来我再执行”。就像电梯门:只要还有人进来,电梯就不会关门,直到最后一个人进来后等待几秒,门才会关上。

1.2 适用场景

  • 搜索框输入联想:用户停止输入后再发送请求
  • 表单验证:用户停止输入后再校验
  • 按钮防连点:防止用户短时间内多次点击提交
  • 窗口 resize:调整结束后再重新计算布局

1.3 手写实现

基础版本:

function debounce(fn, delay) {
  let timer = null;
  return function (...args) {
    // 每次触发都清除上一个定时器,重新计时
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

带立即执行选项的版本:

有时候我们希望第一次触发时立即执行,之后的触发才进行防抖(比如按钮点击,希望第一次点击立刻响应):

function debounce(fn, delay, immediate = false) {
  let timer = null;
  return function (...args) {
    if (timer) clearTimeout(timer);

    if (immediate) {
      // 如果还没有定时器,说明是第一次触发,立即执行
      const callNow = !timer;
      timer = setTimeout(() => {
        timer = null;
      }, delay);
      if (callNow) {
        fn.apply(this, args);
      }
    } else {
      timer = setTimeout(() => {
        fn.apply(this, args);
      }, delay);
    }
  };
}

带取消功能的版本:

在实际项目中,我们可能需要在组件卸载时取消未执行的防抖操作:

function debounce(fn, delay, immediate = false) {
  let timer = null;

  function debounced(...args) {
    if (timer) clearTimeout(timer);

    if (immediate) {
      const callNow = !timer;
      timer = setTimeout(() => {
        timer = null;
      }, delay);
      if (callNow) {
        fn.apply(this, args);
      }
    } else {
      timer = setTimeout(() => {
        fn.apply(this, args);
      }, delay);
    }
  }

  // 添加取消方法
  debounced.cancel = function () {
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }
  };

  return debounced;
}

二、节流(Throttle)

2.1 原理

节流的核心思想是:在单位时间内,事件回调最多执行一次。 无论事件触发多少次,都保证在指定的时间间隔内只执行一次。

简单来说,就是“我不管你触发多快,我按固定频率执行”。就像水龙头滴水,无论水管里水流多急,滴水的频率是固定的。

2.2 适用场景

  • 滚动加载更多:每隔一段时间检测一次滚动位置
  • 拖拽移动:按固定频率更新位置
  • 游戏中的射击:限制射击频率
  • 高频点击提交:限制提交频率

2.3 手写实现

时间戳版本(首次立即执行):

function throttle(fn, interval) {
  let lastTime = 0;
  return function (...args) {
    const now = Date.now();
    // 距离上次执行超过了 interval 才执行
    if (now - lastTime >= interval) {
      lastTime = now;
      fn.apply(this, args);
    }
  };
}

定时器版本(首次延迟执行):

function throttle(fn, interval) {
  let timer = null;
  return function (...args) {
    if (timer) return; // 还在冷却中,直接忽略
    timer = setTimeout(() => {
      fn.apply(this, args);
      timer = null;
    }, interval);
  };
}

时间戳 + 定时器结合版(首次立即执行 + 最后一次也执行):

这是最完善的版本,既保证首次立即执行,又保证事件停止触发后还能执行最后一次:

function throttle(fn, interval) {
  let lastTime = 0;
  let timer = null;

  return function (...args) {
    const now = Date.now();
    const remaining = interval - (now - lastTime);

    // 如果距离上次执行已超过 interval,立即执行
    if (remaining <= 0) {
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      lastTime = now;
      fn.apply(this, args);
    } else if (!timer) {
      // 否则设置定时器,保证最后一次也能执行
      timer = setTimeout(() => {
        lastTime = Date.now();
        timer = null;
        fn.apply(this, args);
      }, remaining);
    }
  };
}

三、防抖 vs 节流:核心区别

特性 防抖(Debounce) 节流(Throttle)
执行时机 事件停止触发后延迟执行 固定时间间隔内执行一次
触发频率 不固定,取决于用户停止操作的时间 固定,按设定间隔执行
类比 电梯门(等人进完再关) 红绿灯(固定时间切换)
适用场景 搜索联想、表单验证 滚动加载、拖拽、高频点击

一句话总结:防抖是“等你不动了我再动”,节流是“你动你的,我按我的节奏动”。

四、实际应用示例

搜索框防抖

const searchInput = document.getElementById('search');

function fetchSuggestions(keyword) {
  console.log('发送请求,搜索:', keyword);
  // 实际项目中这里会发送 AJAX 请求
}

const debouncedSearch = debounce(fetchSuggestions, 500);

searchInput.addEventListener('input', (e) => {
  debouncedSearch(e.target.value);
});

滚动节流

function handleScroll() {
  console.log('当前滚动位置:', window.scrollY);
  // 实际项目中这里可能做懒加载、回到顶部按钮显示等
}

const throttledScroll = throttle(handleScroll, 200);

window.addEventListener('scroll', throttledScroll);

五、使用注意事项

  1. this 指向问题:手写实现中必须用 fn.apply(this, args) 而非直接 fn(args),否则会丢失调用者的 this 上下文。
  2. 参数传递:使用剩余参数 ...args 确保原函数的参数能正确传递。
  3. 取消功能:在组件化开发中(如 React、Vue),组件卸载时需要取消未执行的定时器,避免内存泄漏。
  4. 选择哪种:需要“最后一次”生效用防抖,需要“固定频率”生效用节流。实际开发中也可以结合使用。
  5. lodash 的替代:如果不想手写,可以直接使用 lodash.debouncelodash.throttle,它们提供了更完善的配置项和边界处理。

总结

防抖和节流是前端性能优化中最基础也最重要的两个工具。理解它们的核心原理——防抖是“延迟执行 + 重新计时”,节流是“固定频率执行”——能够帮助我们在面对不同场景时做出正确的选择。手写实现不仅能加深理解,还能在面试中展现扎实的 JavaScript 基础功底。

在实际项目中,建议优先使用成熟的工具库(如 lodash),同时在理解原理的基础上,根据具体业务需求进行定制化实现。

未经允许不得转载:任鹏个人博客 » 防抖与节流:高频事件性能优化的原理与手写实现

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏