学完这篇你能得到两段可以直接复制使用的 debounce / throttle 代码,以及一张「什么场景该用哪个」的对照表,不再靠猜参数。
第一步:先分清防抖和节流到底在解决什么
两者都是用来「降低高频事件的执行次数」,但控制逻辑不同:
- 防抖(debounce):事件触发后开始计时,等待 n 毫秒;如果这期间又触发,就重新计时。结果只有最后一次真正执行。
- 节流(throttle):不管触发多频繁,固定时间窗口内最多执行一次,中间的事件被丢弃。
一句话记忆:防抖是“等你停下来再说”,节流是“按固定节奏说”。
第二步:实现防抖 debounce
最基础版本,保留 this 和参数:
function debounce(fn, delay = 300) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
用法:
const input = document.querySelector('#search');
input.addEventListener('input', debounce(function (e) {
console.log('发请求:', e.target.value);
}, 300));
如果希望首次立即执行(比如按钮防重复点击),加一个 immediate 参数:
function debounce(fn, delay = 300, immediate = false) {
let timer = null;
return function (...args) {
const callNow = immediate && !timer;
clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
if (!immediate) fn.apply(this, args);
}, delay);
if (callNow) fn.apply(this, args);
};
}
注意:包装函数必须写成普通 function,不能写成箭头函数。箭头函数没有自己的 this,在严格模式或模块里 this 是 undefined,fn.apply(this, args) 会把原函数的上下文弄丢。
第三步:实现节流 throttle
时间戳版实现“首次立即执行”,加上定时器补偿,保证最后一次也不丢:
function throttle(fn, interval = 300) {
let last = 0;
let timer = null;
return function (...args) {
const now = Date.now();
const remaining = interval - (now - last);
if (remaining <= 0) {
if (timer) { clearTimeout(timer); timer = null; }
last = now;
fn.apply(this, args);
} else if (!timer) {
timer = setTimeout(() => {
last = Date.now();
timer = null;
fn.apply(this, args);
}, remaining);
}
};
}
用法(滚动加载更多):
window.addEventListener('scroll', throttle(function () {
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 200) {
loadMore();
}
}, 200));
注意:滚动、拖拽、mousemove 这类和渲染相关的场景,优先用 requestAnimationFrame 包一层,比手写 16ms 节流更省事,也不会在后台标签页白白消耗。节流更适合「发请求、写日志、算数据」这类不依赖帧率的操作。
第四步:参数怎么定
- 防抖延迟:搜索联想 300ms 左右;表单校验 400~500ms;
resize 重排 150~300ms。
- 节流间隔:滚动 100~200ms;上报打点 1000ms 以上都可以。
- 延迟设太小等于没优化,设太大用户会觉得卡。宁可先用中等值上线,再根据体验调整。
第五步:应用场景对照
| 场景 | 选择 | 原因 |
|---|
| 搜索框输入联想 | 防抖 300ms | 只在用户停手后发一次请求 |
| 表单实时校验 | 防抖 | 避免边打字边报错 |
| 提交按钮防重复点击 | 防抖 immediate=true | 第一次立刻响应,后续点击被吃掉 |
| 滚动加载 / 吸顶判断 | 节流 200ms | 需要过程反馈,不能等到停下 |
| 拖拽、鼠标跟随 | rAF 或节流 16ms | 跟帧率绑定最顺滑 |
| 埋点上报 | 节流 1000ms | 控制请求量即可 |
注意:给事件解绑时,removeEventListener 必须传包装后的那个引用,不能传原函数,否则解绑无效、内存里还会留着一个定时器。稳妥做法是把包装结果存成变量:const onScroll = throttle(handler, 200);。
小结
- 防抖 = 重新计时,只留最后一次;节流 = 固定窗口,最多一次。
- 实现时用
fn.apply(this, args) 保住上下文和参数,包装函数别用箭头函数。
- 需要“立刻响应 + 后续限流”就用防抖的
immediate 模式或节流。
- 滚动、拖拽类优先考虑
requestAnimationFrame,请求类操作用节流,等待用户输入结束用防抖。
- 解绑事件要保存包装后的引用。