每天都在用的 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)吗?

lodash 的 debounce 支持参数 { 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 引用或变量状态。

解决方法:

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

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

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

所以,再问你一次:

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

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

相关推荐
橙光成长37 分钟前
PDF离线工具箱深度解析:4MB纯前端单文件,40+功能全本地运行,文件绝不上传
前端·pdf·1024程序员节
小poop7 小时前
CSS 基础与实战:从入门到精通网页美化
前端
考研保研资料分享8 小时前
中国人民大学英语笔译保研面经:五校考核、视译口译与择校复盘
面试
Csvn9 小时前
运行时性能
前端
Never_every999 小时前
位图转矢量工具哪个好?实测抠抠图AI位图转矢量工具
大数据·前端
paopaokaka_luck10 小时前
宠物门店经营管理系统(销售与库存联动、进货自动记账、 退换与退款关联、员工个人业绩、ECharts 经营分析、滞销识别与客户分层、宠物档案年龄计算)
前端·javascript·spring boot·mysql·echarts·mybatis·1024程序员节
古希腊掌管代码的神THU10 小时前
为什么DPO中chosen/rejected概率同时下降?
人工智能·深度学习·机器学习·自然语言处理·面试
空中湖10 小时前
用 React 写数据大屏:为什么我不建议用图表库
前端·react.js·前端框架
欣欣之王来了12 小时前
原始HTML插值:v-html指令的使用与安全风险
前端·学习·架构·项目·vue3教程
IT_陈寒12 小时前
Redis的LRU淘汰策略坑了我一整天
前端·人工智能·后端