约 8 分钟读完 · 更新于 2026-10-21 · 文中每条都能在下面的配套工具里动手验证
防抖(debounce):事件停止触发 delay 毫秒后才执行一次。节流(throttle):不管事件触发多频繁,每 interval 毫秒最多执行一次。
核心就两步:每次触发先清掉上一个定时器,再起新定时器。注意用 function 声明以保留 this 和 arguments。
function debounce(fn, delay = 300) {
let timer = null;
return function (...args) {
clearTimeout(timer); // 关键:清掉上一次
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
const onSearch = debounce((kw) => console.log('搜索:', kw), 500);
input.addEventListener('input', (e) => onSearch(e.target.value));
// 500ms 内连续输入,只会触发最后一次只比时间戳的版本会在最后一次触发后漏掉收尾调用,加上尾判定时器补上。
function throttle(fn, interval = 200) {
let last = 0;
let timer = null;
return function (...args) {
const now = Date.now();
const remain = interval - (now - last);
if (remain <= 0) { // 到点,立即执行
last = now;
fn.apply(this, args);
} else if (!timer) { // 没排队就排一个收尾的
timer = setTimeout(() => {
last = Date.now();
timer = null;
fn.apply(this, args);
}, remain);
}
};
}
window.addEventListener('scroll', throttle(() => {
console.log('当前滚动位置:', window.scrollY);
}, 200));记住一行:只需要最后一次结果用防抖,需要过程均匀采样用节流。
| 场景 | 选哪个 | 建议参数 |
|---|---|---|
| 搜索框输入建议 | 防抖 | 300~500ms |
| 表单实时校验 | 防抖 | 300ms |
| 窗口 resize 重算布局 | 防抖或节流 | 200ms |
| 滚动加载 / 吸顶判断 | 节流 | 100~200ms |
| 提交按钮防连点 | 防抖(leading 立即执行) | 500ms |
| 鼠标移动跟随动画 | 节流 | 16ms(对齐 rAF) |
防抖失效几乎都是同一类错误:函数被反复创建。
function debounceLeading(fn, delay = 500) {
let timer = null;
return function (...args) {
const shouldCall = timer === null; // 冷却期内再点不执行
clearTimeout(timer);
timer = setTimeout(() => (timer = null), delay);
if (shouldCall) fn.apply(this, args);
};
}
// 第一次点击立即提交,500ms 内的后续点击全部忽略生产环境直接用 lodash 的 debounce/throttle 也完全合理,注意这几件事。
要「用户停下来再执行」用防抖,如搜索建议、输入校验;要「持续触发但降频」用节流,如滚动加载、resize 重算。只需最后一次结果就用防抖。
九成是在事件回调里写成了 debounce(fn,300)(),每次触发都新建闭包、定时器互不干扰。防抖函数必须在初始化时创建一次并复用。
提交按钮更推荐「请求期间置灰加 loading 锁」;如果只是阻止手抖双击,用 500ms 的 leading 防抖立即执行一次,比节流更符合直觉。
组件每次渲染都重新创建防抖函数,旧闭包里的定时器仍在跑。用 useRef 或 useMemo 依赖空数组缓存函数,卸载时记得 clearTimeout。