JS 代码技巧 vol.3 — 10 个防抖节流深挖,小白到老炮的踩坑路

JS 代码技巧 vol.3 --- 10 个防抖节流深挖,小白到老炮的踩坑路

小不的代码技巧系列第 3 期

主题:防抖 · 节流

面试被问烂的题,但真写对的不多。主打一个"踩坑 → 解法 → 实战"三段式 😎

哈喽哇!我是小不不简说的不~

作为一个在代码界"翻车"无数次的选手,我算是看明白了:坑这东西吧,要么不踩,踩就踩大的😂

今天这期,主打一个 防抖 · 节流------不整虚的,全是实战里哭出来的教训。看不看随你~反正翻车实录又不收钱

Ps:文末有惊喜(不是广告!)


1. 基础防抖(trailing):搜索框的标配

场景:用户输入"前端",理想是只发一次"前端"查询,不是 5 次。

js 复制代码
// 反例:每次按键都请求接口,老板看了想打人
input.addEventListener("input", (e) => {
  fetch(`/api/search?q=${e.target.value}`);
});

正解------最后一次输入后等 300ms 才执行:

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

const search = debounce((q) => fetch(`/api/search?q=${q}`), 300);
input.addEventListener("input", (e) => search(e.target.value));

坑在哪:连点 5 次都不发请求,要等停下来才发------用户需要"边输边查"就抓瞎了。


2. 基础节流(leading):按钮防重点击

场景:用户狂点提交按钮(手速王者),后端被刷到怀疑人生。

js 复制代码
function throttle(fn, wait) {
  let last = 0;
  return function (...args) {
    const now = Date.now();
    if (now - last >= wait) {
      last = now;
      fn.apply(this, args);
    }
  };
}

const submit = throttle(handleSubmit, 1000);
btn.addEventListener("click", submit);

坑在哪:第一次立刻执行,后续 N 毫秒内不响应------用户连点 N 下只有第一下生效。


3. 双剑合璧:leading + trailing 都要

既要第一次立刻响应(按钮体感),又要最后一次也响应(搜索框体感),lodash 的实现就是这种。

js 复制代码
function debounce(fn, wait, options = {}) {
  let timer = null;
  let lastArgs = null,
    lastThis = null;
  let lastCall = 0;

  function invoke() {
    const args = lastArgs,
      context = lastThis;
    lastArgs = lastThis = null;
    fn.apply(context, args);
  }

  function start() {
    lastCall = Date.now();
    timer = setTimeout(timerEnd, wait);
  }

  function timerEnd() {
    const now = Date.now();
    if (options.leading && now - lastCall < wait) {
      timer = setTimeout(timerEnd, wait - (now - lastCall));
    } else {
      timer = null;
      if (!options.trailing) return;
      invoke();
    }
  }

  return function (...args) {
    lastArgs = args;
    lastThis = this;
    if (timer == null) {
      if (options.leading) invoke();
      start();
    } else if (options.trailing) {
      clearTimeout(timer);
      timer = setTimeout(timerEnd, wait);
    }
  };
}

坑在哪 :上面是简化版,真实 lodash 还要处理 maxWait / cancel / flush。生产环境直接 import { debounce } from 'lodash-es' 完事。


4. this 绑定:经典踩坑

js 复制代码
const obj = {
  name: "小不",
  search(q) {
    console.log(this.name, q);
  },
};

input.addEventListener("input", obj.search); // ❌ this 是 input
input.addEventListener("input", debounce(obj.search, 300)); // ❌ this 还是 input
input.addEventListener(
  "input",
  debounce(() => obj.search(), 300),
); // ✓ this 是 obj

坑在哪setTimeout 里的 this 默认是 window(严格模式是 undefined),所以 debounce 内部必须用 fn.apply(this, args) 保留原 this。箭头函数那一招是绕过去,不是真解。


5. 立即执行:第一次也要传参

搜索框想要"边输边查 + 停下来再查"------leading + trailing 都开。但有时候只要 leading:

js 复制代码
debounce(search, 300, { leading: true, trailing: false });

坑在哪leading 模式下 fn 立即执行时拿到的不是最新参数,是上一次保存的 lastArgs,要小心取数时机。


6. 取消机制:必须能 abort

经典场景:组件卸载了,请求还在排队;用户跳页了,防抖还没触发。

js 复制代码
const search = debounce(q => fetch(...), 300);
search('前端');   // 用户立刻跳页
search.cancel();  // 取消这次排队

自己实现加个 cancel

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

坑在哪 :React 组件 useEffect 里不取消,unmount 后 setState 会报 "Can't perform a React state update on an unmounted component"------经典 warning 来源。


7. maxWait:lodash 的精髓

纯 debounce 有个极端场景:用户一直输,每 300ms 内都有新输入------结果一次请求都不发,请求直接饿死。

js 复制代码
const search = debounce(fn, 300, { maxWait: 1000 });

逻辑:不管你怎么触发,最多等 1000ms 强制执行一次。

坑在哪:maxWait 顾名思义有上限,配置不当会变成"看起来防抖了但还是 N ms 一次请求"。


8. requestAnimationFrame 节流:滚动更顺滑

js 复制代码
let ticking = false;
window.addEventListener("scroll", () => {
  if (!ticking) {
    requestAnimationFrame(() => {
      // 滚动逻辑
      ticking = false;
    });
    ticking = true;
  }
});

跟 16ms 节流(≈60fps)效果差不多,但 rAF 会跟着浏览器刷新率走,更智能。

坑在哪:tab 切到后台 rAF 暂停,恢复后才执行------别用来做"心跳包",关键时刻掉链子。


9. scroll + passive:性能关键

js 复制代码
// ❌ 阻塞滚动,浏览器得等回调跑完才滚
window.addEventListener("scroll", handler);

// ✓ 告诉浏览器我不调用 preventDefault,你放心滚
window.addEventListener("scroll", handler, { passive: true });

移动端差别尤其大,能省 100ms+ 滚动延迟。

坑在哪 :用了 passive: true 就别想 e.preventDefault() 了------浏览器直接无视。


10. React useEffect 里的防抖:闭包陷阱

jsx 复制代码
function Search() {
  const [query, setQuery] = useState("");

  useEffect(() => {
    const fetch_ = debounce(() => {
      console.log("查:", query); // ❌ 永远是初始空字符串
    }, 300);
    return () => fetch_.cancel();
  }, []); // 依赖空
}

正解------把 query 放进依赖:

jsx 复制代码
useEffect(() => {
  const fetch_ = debounce(() => {
    console.log("查:", query); // ✓ 拿到最新值
  }, 300);
  return () => fetch_.cancel();
}, [query]);

坑在哪 :依赖一改就重新创建 debounce,成本不大但每次新建要 .cancel() 旧的,循环引用过深会泄漏。更稳的写法是 useRef 保存 debounced 函数本体,effect 里只更新参数。


📦 收个尾

这 10 个技巧浓缩一下:

  • 最高频踩坑 :debounce 拿不到 this、useEffect 闭包陷阱、forEach + async 顺序玄学
  • 最值得收藏 :leading + trailing 双剑合璧、scroll + passive 性能优化
  • 核心思路:区分防抖(去重)和节流(限流)的本质

学到了就是赚到了,犹豫徘徊等于白来~


写到最后

想要啥技巧?评论区甩个题目过来~

  • 你刚踩的坑
  • 项目里反复写的代码
  • 想搞清楚但一直懒得查的 API

小不看到...不一定回 😂 毕竟代码里翻车太多,腾不出手~

Ps:三连随缘,催更的会被打 😂

相关推荐
小智社群1 小时前
VR漫游视频完整版
前端·javascript·音视频
浩哥学JavaAI1 小时前
2026年最新AI agent面试(05)_RAG基础应用
人工智能·面试·职场和发展
不知名的老吴2 小时前
算法程序员常考大厂面试题汇总解析
面试·职场和发展
浩瀚地学2 小时前
【面试算法笔记】0302-哈希表-哈希表实现
java·经验分享·笔记·算法·面试
小程故事多_802 小时前
拆解SOP+ReAct对话Agent,告别硬编码,解锁智能对话的柔性迭代能力
前端·人工智能·react.js·前端框架
lzjava20242 小时前
LangChain Prompt提示词工程
前端·langchain·prompt
nicole bai2 小时前
vue 自定义模块内容
前端·javascript·vue.js
清水白石0083 小时前
Python 类定义阶段自动注册子类:从 `__init_subclass__` 到插件系统实战
linux·前端·python
西西学代码3 小时前
Flutter---StreamBuilder案例
javascript·windows·flutter