这套代码基本可以直接进项目了,唯一要补的是时间戳
防抖与节流:高频事件的正确打开方式
学完这篇你能搞清防抖和节流到底解决什么问题、怎么写、什么时候用哪个,并拿到几段可以直接复制进项目的代码。
前端有一类事件天生"话多":input 每敲一个字符触发一次、scroll 每滑动一像素触发一次、resize 拖动窗口时每秒能触发几十次、按钮被连点五下就是五次提交。这些高频事件不做处理,轻则卡顿,重则把接口打崩。防抖(debounce)和节流(throttle)就是两把最常用的"限流阀"。
第一步:先分清两者到底在干什么
一句话记住区别:
- 防抖:你不停地触发,我就一直等,直到你停下来 N 毫秒后才执行一次。适合"等用户操作完再干活"。
- 节流:不管你触发多快,我固定每 N 毫秒最多执行一次。适合"过程中要持续有反馈"。
典型场景对照:搜索框输入联想、表单校验、窗口 resize 结束时重算布局 → 用防抖;滚动加载更多、拖拽元素跟随、鼠标移动绘制 → 用节流。
第二步:写一个能用的 debounce
function debounce(fn, delay = 300, immediate = false) {
let timer = null;
function debounced(...args) {
const callNow = immediate && timer === null;
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
if (!immediate) fn.apply(this, args);
}, delay);
if (callNow) fn.apply(this, args);
}
// 需要时手动取消(比如组件卸载、切换页面)
debounced.cancel = function () {
clearTimeout(timer);
timer = null;
};
return debounced;
}
// 用法
const onInput = debounce((e) => {
console.log('搜索:', e.target.value);
}, 300);
input.addEventListener('input', onInput);
`immediate = true` 时首次触发立刻执行一次(比如按钮防止连点),之后必须等安静下来才能再触发。
注意:`fn.apply(this, args)` 不能省成 `fn()`。否则在类或 Vue 组件里 `this` 会丢,`e` 也传不进去,报错信息还特别难看懂。
第三步:写一个 throttle
先看最简的"时间戳版":
function throttle(fn, interval = 300) {
let last = 0;
return function (...args) {
const now = Date.now();
if (now - last >= interval) {
last = now;
fn.apply(this, args);
}
};
}
它第一次立即执行,优点是干脆;缺点是最后一次触发如果没跨过时间点,会被丢掉——滚动到底部时最后那一下最容易丢。
于是有"定时器版":
function throttle(fn, interval = 300) {
let timer = null;
return function (...args) {
if (timer) return;
timer = setTimeout(() => {
timer = null;
fn.apply(this, args);
}, interval);
};
}
它保证最后一次一定执行,但第一次会延迟。想要"首次立即执行 + 最后一次也执行",用下面这版:
function throttle(fn, interval = 300, { leading = true, trailing = true } = {}) {
let last = 0, timer = null;
return function (...args) {
const now = Date.now();
if (!last && !leading) last = now;
const remaining = interval - (now - last);
if (remaining <= 0 || remaining > interval) {
if (timer) { clearTimeout(timer); timer = null; }
last = now;
fn.apply(this, args);
} else if (!timer && trailing) {
timer = setTimeout(() => {
last = leading ? Date.now() : 0;
timer = null;
fn.apply(this, args);
}, remaining);
}
};
}
注意:如果只是业务里用,直接装 lodash 用 `_.debounce` / `_.throttle` 完全没问题,它们支持 `leading`、`trailing`、`cancel`、`flush`,别为了"不用库"把上面这段抄错一个变量。
第四步:三个高频坑
坑一:搜索接口的竞态。 防抖只是减少了请求数,慢请求仍然可能后发先至。正确做法是配合 `AbortController`:
let controller = null;
const search = debounce(async (kw) => {
controller?.abort();
controller = new AbortController();
const res = await fetch(`/api/search?q=${encodeURIComponent(kw)}`, {
signal: controller.signal,
});
render(await res.json());
}, 300);
坑二:把该实时更新的状态也防抖了。 输入框里被防抖的应该是"发请求"这个动作,输入框自身的值必须实时变化,否则用户会觉得打字卡顿。在 Vue/React 里就是:数据照常绑定,只对回调做防抖。
坑三:防抖/节流函数在组件里重复创建。 每次渲染都 `debounce(...)` 会生成一个全新的函数,`clearTimeout` 也就清不掉旧的,等于没生效。要用 `useMemo`、`setup` 顶层变量或组件外定义把它固定下来。
额外一招:滚动和拖拽优先用 `requestAnimationFrame`。 屏幕刷新率一般 60Hz,用 rAF 节流天然对齐帧,比手写 16ms 更顺滑:
let ticking = false;
window.addEventListener('scroll', () => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
doSomething();
ticking = false;
});
});
小结
- 防抖 = 等停止后执行一次,管"最终结果";节流 = 固定频率执行,管"过程反馈"。
- 写的时候别丢 `this` 和参数,用 `fn.apply(this, args)`。
- 防抖记得留 `cancel`,节流按需选 leading/trailing。
- 搜索框防抖 + AbortController 是标配,光防抖不取消请求仍会串数据。
- 滚动、动画类回调优先 rAF;防抖/节流函数不要在每次渲染时重建。
- 拿不准就上 lodash,把精力留给业务逻辑。
转载请注明出处,版权归原作者所有。
全部回复 2
yipeng:这套代码基本可以直接进项目了,唯一要补的是时间戳
对,时间戳版最大的坑就是「尾调用丢失」——滚动到底部那一次如果没跨过 interval,`fn` 就永远不会执行,「加载更多」直接失灵。补法不是加时间戳,而是时间戳 + `setTimeout` 双轨:到点立即执行,没到点就挂一个定时器兜底最后一次。
function throttle(fn, interval = 300, { leading = true, trailing = true } = {}) {
let last = 0, timer = null;
function throttled(...args) {
const now = Date.now();
if (!last && !leading) last = now;
const remaining = interval - (now - last);
if (remaining <= 0) {
if (timer) { clearTimeout(timer); timer = null; }
last = now;
fn.apply(this, args);
} else if (!timer && trailing) {
timer = setTimeout(() => {
last = leading ? Date.now() : 0;
timer = null;
fn.apply(this, args);
}, remaining);
}
}
throttled.cancel = () => { clearTimeout(timer); timer = null; last = 0; };
return throttled;
}
用法上:滚动加载、输入联想保留 `leading + trailing`;拖拽跟随、鼠标绘制建议 `trailing: false`,否则松手后会「回弹」跳一下。组件卸载记得 `cancel()`,不然定时器可能引用已销毁的 DOM。
一个小注意点:`last` 用 `Date.now()` 就别和 `performance.now()` 混用,两者起点不同会算出负值;另外 `
正式会员
认证极客
见习用户





