JS的防抖与节流

本文将从核心思想→基础实现→生产级代码→应用场景→面试考点全链路讲解,所有代码均可直接复制到项目中使用。


前言

你一定遇到过这些场景:

  • 输入框实时搜索,每输入一个字符就发一次请求
  • 窗口 resize 时,页面疯狂重绘导致卡顿
  • 滚动加载更多,滚动一次触发十次接口
  • 按钮快速点击导致表单重复提交

这些问题的本质都是:短时间内高频触发的函数,造成了不必要的性能浪费

防抖(Debounce)节流(Throttle),就是解决这类问题最核心、最常用的两个性能优化技术。

一、防抖(Debounce)

1. 核心思想

将短时间内多次触发的函数,合并为最后一次执行

简单说就是:等你停下来,我再执行

2. 基础版实现(理解原理)

这是最容易理解的核心逻辑,适合新手入门:

javascript 复制代码
let timer = null;
input.addEventListener('keyup', function() {
  // 每次触发都清除之前的定时器
  if (timer) clearTimeout(timer);
  // 重新开始计时
  timer = setTimeout(() => {
    console.log('发送搜索请求');
  }, 500);
});

缺点:全局变量污染、不可复用、this 指向错误、无法传递参数。

3. 生产级实现(推荐直接使用)

解决了基础版的所有问题,支持立即执行 / 非立即执行双模式,自带取消功能:

javascript 复制代码
/**
 * 防抖函数
 * @param {Function} fn - 需要防抖的目标函数
 * @param {number} delay - 延迟时间(毫秒)
 * @param {boolean} immediate - 是否立即执行(默认:false)
 * @returns {Function} 防抖后的函数,自带cancel方法
 */
function debounce(fn, delay, immediate = false) {
  let timer = null;

  const debounced = function(...args) {
    // 保存正确的this上下文
    const context = this;
    // 每次触发都清除之前的定时器
    if (timer) clearTimeout(timer);

    if (immediate) {
      // 立即执行模式:只有当没有定时器时才执行
      const callNow = !timer;
      // 定时器仅负责重置状态,不执行函数
      timer = setTimeout(() => {
        timer = null;
      }, delay);
      // 满足条件则立即执行
      if (callNow) fn.apply(context, args);
    } else {
      // 非立即执行模式:最后一次触发后执行
      timer = setTimeout(() => {
        fn.apply(context, args);
        timer = null;
      }, delay);
    }
  };

  // 手动取消未执行的防抖
  debounced.cancel = function() {
    clearTimeout(timer);
    timer = null;
  };

  return debounced;
}

// 处理函数
function handle() {
  console.log(Math.random());
}

// resize事件
window.addEventListener("resize", debounce(handle, 1000));

4. 两种执行模式

模式 行为 适用场景
immediate: false(默认) 最后一次触发后,等待delay毫秒再执行 输入框搜索、自动保存
immediate: true 第一次触发立即执行,之后delay毫秒内的触发都被忽略;最后一次触发后,不会再执行! 按钮防重复点击、提交操作

效果如下: 我们可以看到,当持续触发resize事件时,事件处理函数handle只在停止滚动1000毫秒之后才会调用一次,也就是说在持续触发resize事件的过程中,事件处理函数handle一直没有执行。

二、节流(Throttle)

1. 核心思想

保证函数在固定时间间隔内,最多只执行一次

简单说就是:不管你触发多少次,我每隔固定时间只执行一次

2. 生产级实现(推荐直接使用)

这是目前最标准、功能最完整的节流实现,支持leading/trailing双配置:

ini 复制代码
/**
 * 节流函数
 * @param {Function} fn - 需要节流的目标函数
 * @param {number} delay - 时间间隔(毫秒)
 * @param {Object} options - 配置项
 * @param {boolean} options.leading - 是否在开始时执行(默认:true)
 * @param {boolean} options.trailing - 是否在结束时补执行一次(默认:true)
 * @returns {Function} 节流后的函数,自带cancel方法
 */
function throttle(fn, delay, options = { leading: true, trailing: true }) {
  const { leading, trailing } = options;
  let timer = null;
  let lastTime = 0;

  const throttled = function(...args) {
    const context = this;
    const now = Date.now();

    // 处理不立即执行的情况
    if (!leading && !lastTime) {
      lastTime = now;
    }

    // 时间差达到间隔,立即执行
    if (now - lastTime >= delay) {
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      fn.apply(context, args);
      lastTime = now;
    } 
    // 最后一次触发后,补执行一次
    else if (trailing && !timer) {
      timer = setTimeout(() => {
        fn.apply(context, args);
        lastTime = leading ? Date.now() : 0;
        timer = null;
      }, delay - (now - lastTime));
    }
  };

  // 手动取消节流
  throttled.cancel = function() {
    clearTimeout(timer);
    timer = null;
    lastTime = 0;
  };

  return throttled;
}

3. 四种组合模式

配置 行为 适用场景
leading: true, trailing: true(默认) 开始执行一次,结束再补一次 滚动加载更多、窗口 resize
leading: true, trailing: false 只在开始执行一次 按钮点击、鼠标移动
leading: false, trailing: true 只在结束执行一次 拖拽元素位置更新
leading: false, trailing: false 无意义,不推荐使用 -

三、防抖 vs 节流:一张表搞懂区别

特性 防抖(Debounce) 节流(Throttle)
核心逻辑 最后一次执行 每隔固定时间执行一次
执行次数 高频触发下只执行 1 次 高频触发下执行多次(固定频率)
适用场景 等待用户操作结束后再执行 需要保证一定执行频率的场景
典型应用 输入框搜索、自动保存、按钮防重复点击 滚动加载、窗口 resize、拖拽、动画

一句话总结

  • 防抖:适合 "等你停下来再做" 的场景
  • 节流:适合 "每隔一段时间做一次" 的场景

四、实际使用示例

1. 输入框实时搜索(防抖)

javascript 复制代码
const searchInput = document.querySelector('.search-input');

function handleSearch(e) {
  console.log('发送搜索请求:', e.target.value);
  // 实际项目中这里调用接口
}

// 停止输入500ms后再发送请求
searchInput.addEventListener('keyup', debounce(handleSearch, 500));

2. 按钮防重复点击(防抖 - 立即执行)

javascript 复制代码
const submitBtn = document.querySelector('.submit-btn');

function handleSubmit() {
  console.log('提交表单');
  // 实际项目中这里调用提交接口
}

// 点击后立即执行,3秒内再次点击无效
submitBtn.addEventListener('click', debounce(handleSubmit, 3000, true));

3. 滚动加载更多(节流)

javascript 复制代码
function handleScroll() {
  // 判断是否滚动到底部
  if (window.innerHeight + window.scrollY >= document.body.offsetHeight) {
    console.log('加载更多数据');
    // 实际项目中这里调用加载更多接口
  }
}

// 每隔200ms执行一次,避免频繁触发
window.addEventListener('scroll', throttle(handleScroll, 200));

4. 框架中使用(React/Vue)

重要:组件卸载时一定要调用 cancel 方法,避免内存泄漏

javascript 复制代码
// React示例
useEffect(() => {
  const handleResize = throttle(() => {
    console.log('窗口大小变化');
  }, 200);

  window.addEventListener('resize', handleResize);

  // 组件卸载时取消节流
  return () => {
    handleResize.cancel();
  };
}, []);

五、常见误区与踩坑点

1. 最经典的坑:事件绑定加括号

javascript

运行

javascript 复制代码
// ❌ 错误:加了括号,函数会立即执行,不会绑定事件
window.addEventListener('resize', handle());

// ✅ 正确:不加括号,传递函数本身
window.addEventListener('resize', handle);

// ✅ 正确:防抖/节流写法也是一样
window.addEventListener('resize', debounce(handle, 500));

2. 分不清防抖和节流

  • 输入框搜索:用防抖(等用户输完再搜)
  • 滚动加载:用节流(每隔一段时间加载一次)
  • 按钮防重复点击:用防抖(立即执行模式)

3. 组件卸载时不取消定时器

会导致内存泄漏,尤其是在单页应用中,一定要在组件卸载时调用cancel()方法。


六、高频面试题汇总

1. 什么是防抖和节流?它们有什么区别?

答:见本文第三部分。

2. 手写防抖函数

答:见本文第一部分生产级实现。

3. 手写节流函数

答:见本文第二部分生产级实现。

4. 防抖和节流的应用场景有哪些?

答:见本文第四部分。

5. 为什么防抖和节流要用闭包?

答:为了封装定时器状态(timer、lastTime),避免全局变量污染,同时保证每个防抖 / 节流函数都有自己独立的状态,互不干扰。


七、总结

防抖和节流是前端开发中最基础也最重要的性能优化技术,掌握它们不仅能解决实际开发中的性能问题,也是面试中的必考点。

本文提供的防抖和节流实现,是目前行业内最标准、最健壮的版本,可以直接复制到任何项目中使用。

最后一句话

能用 Lodash 的_.debounce_.throttle就直接用,它们经过了大量生产环境的验证。但你必须理解它们的原理,这样遇到问题时才能快速排查。

相关推荐
candyTong1 小时前
如何写一个可以进化的前端系统验收 SKILL
javascript
Amy_yang2 小时前
uni-app 中 web-view 的使用与 App 端全屏问题处理
前端·javascript·vue.js
之歆2 小时前
DAY_17深度博客:CSS 响应式布局 · BFC · JavaScript 完全指南(上)
javascript·js
Highcharts.js2 小时前
Highcharts 纯 JavaScript 图表库深度使用评测
开发语言·前端·javascript·功能测试·ecmascript·highcharts·技术评测
ZC跨境爬虫2 小时前
跟着 MDN 学 HTML day_39:(DOMException 异常接口完全解析)
前端·javascript·html·媒体
用户11489669441053 小时前
Promise解析
javascript·面试
gogoing3 小时前
Prettier 配置说明
前端·javascript
前端毕业班3 小时前
uni-app onShareAppMessage hook 原理分析
前端·javascript