每天都在用的 debounce 和 throttle,其实 80% 的用法都错了

"我知道 debounce 是防抖,throttle 是节流,不就是 lodash 那两个方法吗?"

如果你也是这么理解的,那你可能已经在项目中踩了无数坑,而自己还浑然不觉。

防抖和节流的本质,从来就不只是"控制触发频率"这么简单。真正的难点是:时机控制、副作用处理、与业务场景的契合度,以及在框架(React/Vue)中的正确姿势。

这篇文章,我讲大白话,带你重新认识 debounce 和 throttle,并用多个真实场景告诉你:大多数人用错的,不是函数,而是场景。


1. 防抖与节流的核心区别(别再背定义了)

很多文章喜欢这么写:

  • debounce(防抖):N 秒内只执行最后一次
  • throttle(节流):N 秒内最多执行一次

这些话对,但不够用。我们换个工程师视角:

特性 debounce throttle
触发控制 延迟触发,持续打断 定期触发,不受打断
场景强调 防止过度触发 保证持续响应
常用场景 输入框搜索、resize、scroll 结束 滚动监听、鼠标拖动、窗口 resize 实时更新

举个例子你就懂了:

  • 防抖 debounce 适合"等你说完我再处理"

    • 比如搜索框输入,不想每次敲字就请求 API。
  • 节流 throttle 适合"我定时采集信息"

    • 比如用户拖动地图时,定时获取当前中心点。

2. 你真的写对 debounce 吗?这才是正确的 debounce:

js 复制代码
function debounce(fn, delay) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

但重点不是这段代码,而是下面这几点使用注意:

❌ 错误用法:在组件外部声明 debounce 函数

js 复制代码
const search = debounce((val) => fetchData(val), 500);
<input onInput={(e) => search(e.target.value)} />

看起来很标准对吧?错!

如果你在 React/Vue 的组件中这么用,就会出现闭包引用过期、状态无法同步的问题。

✅ 正确用法:将 debounce 放入 useCallback / onMounted 中注册

React 写法:

js 复制代码
const search = useCallback(
  debounce((val) => fetchData(val), 500),
  []
);

Vue 写法:

js 复制代码
setup() {
  const search = debounce((val) => fetchData(val), 500);
  return { search };
}

3. throttle 不能滥用,特别是在动画中

看看下面这段代码:

js 复制代码
window.addEventListener('scroll', throttle(handleScroll, 100));

听起来好像挺对?但如果你在页面上实现动画或懒加载,用户滚动飞快时,内容根本来不及加载完。

为什么?因为 throttle 会限制响应频率,导致 UI 不流畅。

正确做法是------使用 requestAnimationFrame 替代 throttle:

js 复制代码
let ticking = false;
window.addEventListener('scroll', () => {
  if (!ticking) {
    requestAnimationFrame(() => {
      handleScroll();
      ticking = false;
    });
    ticking = true;
  }
});

这种做法被称为 "基于帧节流",更平滑,适合 UI 性能场景。


4. 高级场景:你考虑过立即执行(leading)和延迟执行(trailing)吗?

lodashdebounce 支持参数 { leading: true, trailing: false }

js 复制代码
const fn = debounce(() => console.log('run'), 1000, { leading: true });

解释:

  • leading: true:第一次立刻执行
  • trailing: true:结束后再执行一次

很多人默认 trailing=true,但在某些操作中,如"按钮点击防连点",你应该只允许 leading:true,trailing:false

js 复制代码
const handleClick = debounce(() => doSomething(), 2000, {
  leading: true,
  trailing: false
});

避免按钮在短时间内被连续触发,但第一下仍立即响应。


5. 一次真实的业务踩坑:搜索接口请求丢失

你是否遇到过这种问题:

  • 输入很快时,搜索请求 A、B、C 同时触发
  • 最后 C 请求慢,A 反而先回来了
  • 页面结果却被 A 的结果覆盖了

这时候,你用 debounce 根本没用!

你需要的是:请求去抖 + 响应乱序保护(防抖+防乱序)

js 复制代码
let lastCallId = 0;

const search = debounce(async (val) => {
  const callId = ++lastCallId;
  const res = await fetchSearch(val);
  if (callId === lastCallId) {
    renderResult(res);
  }
}, 300);

6. 自定义 debounce + 取消能力

js 复制代码
function debounce(fn, delay) {
  let timer;
  const debounced = function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
  debounced.cancel = () => clearTimeout(timer);
  return debounced;
}

在 React 的 useEffect 清理中使用:

js 复制代码
useEffect(() => {
  const fn = debounce(doSearch, 300);
  fn();
  return () => fn.cancel();
}, [searchKey]);

避免组件销毁后还触发 debounce 的回调。


7. 面试加分题:防抖/节流为什么容易产生内存泄漏?

答案: 因为他们使用了闭包。如果你在组件中创建函数但不取消,计时器引用就会长期保留旧的 DOM 引用或变量状态。

解决方法:

  • 尽量使用 useCallbackuseRef 管理
  • 提供 .cancel() 接口
  • 在组件卸载时清理

😀不是用对了,而是用得刚好

80% 的人使用 debounce 和 throttle,只满足了"不报错"的最低要求。

所以,再问你一次:

你是真的在用 debounce,还是只是在 copy 它?

📌 你可以继续看我的《为什么》系列文章

相关推荐
豐儀麟阁贵8 分钟前
8.5在方法中抛出异常
java·开发语言·前端·算法
程序员念姐11 分钟前
软件测试系统流程和常见面试题
测试工具·面试
zengyuhan50337 分钟前
Windows BLE 开发指南(Rust windows-rs)
前端·rust
Bro_cat37 分钟前
Java基础
java·开发语言·面试
醉方休40 分钟前
Webpack loader 的执行机制
前端·webpack·rust
前端老宋Running1 小时前
一次从“卡顿地狱”到“丝般顺滑”的 React 搜索优化实战
前端·react.js·掘金日报
隔壁的大叔1 小时前
如何自己构建一个Markdown增量渲染器
前端·javascript
用户4445543654261 小时前
Android的自定义View
前端
WILLF1 小时前
HTML iframe 标签
前端·javascript
枫,为落叶1 小时前
Axios使用教程(一)
前端