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:三连随缘,催更的会被打 😂

相关推荐
weixin_4316004411 小时前
做 Agent 会用到的 Node API(3):异步与流
前端·学习·ai·agent·ai编程
程序员黑豆11 小时前
鸿蒙应用开发之生命周期方法完全指南
前端·harmonyos
一点一木11 小时前
第 23 届 ChinaJoy 刚结束,我把 965 个展台做成了 3D 云展馆
前端·人工智能
wordbaby11 小时前
前端请求缓存写了,同一个接口为什么还是打了十几次?
前端·promise
不可能掉发11 小时前
Env Guard:让浏览器一眼分清生产、测试和开发环境
前端·javascript·chrome·测试工具·html·开源软件·个人开发
饼干哥哥11 小时前
Codex 必改的8 个基础配置
前端·人工智能·后端
扯蛋43811 小时前
langchain1.x 时代的记忆系统 (二)
javascript·llm·agent
ClouGence11 小时前
AI Agent 能写测试、跑流程,为什么回归测试还不能完全交给 AI?
前端·测试
码云之上11 小时前
Context Engineering:让 Agent 在当前步骤看到正确的事实
前端·人工智能·前端工程化
烂蜻蜓11 小时前
Node.js入门教程(一):初识Node.js——服务端JavaScript的诞生与特点
开发语言·javascript·node.js