防抖和节流的区别一次说清:搜索框用防抖、滚动监听用节流(附 10 行手写实现)

防抖是「停下来了才执行」,节流是「固定频率执行」——搜索框要防抖,滚动监听要节流。本文给出一眼能懂的手写实现(含立即执行版),并整理最常见的翻车原因:在事件回调里现场创建防抖函数,等于没防抖。

约 8 分钟读完 · 更新于 2026-10-21 · 文中每条都能在下面的配套工具里动手验证

一句话结论:防抖等停止,节流管频率

防抖(debounce):事件停止触发 delay 毫秒后才执行一次。节流(throttle):不管事件触发多频繁,每 interval 毫秒最多执行一次。

防抖:10 行手写

核心就两步:每次触发先清掉上一个定时器,再起新定时器。注意用 function 声明以保留 this 和 arguments。

搜索框场景
function debounce(fn, delay = 300) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer); // 关键:清掉上一次
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

const onSearch = debounce((kw) => console.log('搜索:', kw), 500);
input.addEventListener('input', (e) => onSearch(e.target.value));
// 500ms 内连续输入,只会触发最后一次

节流:首尾都触发的稳妥版

只比时间戳的版本会在最后一次触发后漏掉收尾调用,加上尾判定时器补上。

滚动监听场景
function throttle(fn, interval = 200) {
  let last = 0;
  let timer = null;
  return function (...args) {
    const now = Date.now();
    const remain = interval - (now - last);
    if (remain <= 0) { // 到点,立即执行
      last = now;
      fn.apply(this, args);
    } else if (!timer) { // 没排队就排一个收尾的
      timer = setTimeout(() => {
        last = Date.now();
        timer = null;
        fn.apply(this, args);
      }, remain);
    }
  };
}

window.addEventListener('scroll', throttle(() => {
  console.log('当前滚动位置:', window.scrollY);
}, 200));

场景决策表

记住一行:只需要最后一次结果用防抖,需要过程均匀采样用节流。

场景选哪个建议参数
搜索框输入建议防抖300~500ms
表单实时校验防抖300ms
窗口 resize 重算布局防抖或节流200ms
滚动加载 / 吸顶判断节流100~200ms
提交按钮防连点防抖(leading 立即执行)500ms
鼠标移动跟随动画节流16ms(对齐 rAF)

防抖「不生效」排查清单

防抖失效几乎都是同一类错误:函数被反复创建。

  1. 检查是不是在事件回调里写 debounce(fn, 300)():每次触发都新建闭包,定时器互不相干,等于没防抖。要在初始化时创建一次。
  2. 用了箭头函数接住回调导致 this 指向丢失:手写版记得用 function 声明并 apply(this, args)。
  3. 组件每次渲染都重新执行 debounce(fn):React 里用 useRef 缓存,Vue 里放在 setup 外或用 watch 自带的 debounce 选项。
  4. 用户反馈「点一下没反应」:那是 trailing 防抖的特性,按钮场景要 leading 版本(见下一段)。

想要「第一次立即执行」:leading 版防抖

可复制的立即执行版
function debounceLeading(fn, delay = 500) {
  let timer = null;
  return function (...args) {
    const shouldCall = timer === null; // 冷却期内再点不执行
    clearTimeout(timer);
    timer = setTimeout(() => (timer = null), delay);
    if (shouldCall) fn.apply(this, args);
  };
}
// 第一次点击立即提交,500ms 内的后续点击全部忽略

进阶与清理

生产环境直接用 lodash 的 debounce/throttle 也完全合理,注意这几件事。

  • lodash 版用 { leading: true, trailing: false } 就能拿到立即执行效果,不必手写重复轮子。
  • 组件卸载时调用返回函数的 .cancel()(lodash)或 clearTimeout,防止回调打到已销毁的 DOM。
  • 旧版 React 的事件池会让防抖回调里读到的 e.target 失效,需要在监听器里先 e.persist()。
  • 单测里验证防抖记得用 vi.useFakeTimers() 这类时间模拟,真实 sleep 会拖慢 CI。

常见问题

防抖和节流什么时候用哪个?

要「用户停下来再执行」用防抖,如搜索建议、输入校验;要「持续触发但降频」用节流,如滚动加载、resize 重算。只需最后一次结果就用防抖。

为什么我的防抖函数完全没效果?

九成是在事件回调里写成了 debounce(fn,300)(),每次触发都新建闭包、定时器互不干扰。防抖函数必须在初始化时创建一次并复用。

按钮防连点用防抖还是节流?

提交按钮更推荐「请求期间置灰加 loading 锁」;如果只是阻止手抖双击,用 500ms 的 leading 防抖立即执行一次,比节流更符合直觉。

React 里防抖为什么会失效?

组件每次渲染都重新创建防抖函数,旧闭包里的定时器仍在跑。用 useRef 或 useMemo 依赖空数组缓存函数,卸载时记得 clearTimeout。

📂

松开以自动格式化

支持 .json / .sql / .csv / .html / .css / .xml / .yaml 文件