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