防抖与节流
JavaScript防抖(debounce)与节流(throttle)详解:原理、实现与实际应用。
1. 防抖(Debounce)
1.1 原理
防抖:事件触发后等待一段时间再执行,如果在等待期间再次触发则重新计时。
触发: ─A─B─C───────D──────→
执行: ─────────────────E──→ (D 之后等待 delay 才执行)
1.2 基本实现
function debounce(fn, delay) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
1.3 支持立即执行
function debounce(fn, delay, immediate = false) {
let timer = null;
return function (...args) {
if (timer) clearTimeout(timer);
if (immediate && !timer) {
fn.apply(this, args);
}
timer = setTimeout(() => {
if (!immediate) {
fn.apply(this, args);
}
timer = null;
}, delay);
};
}
1.4 支持取消和取值
function debounce(fn, delay, immediate = false) {
let timer = null;
const debounced = function (...args) {
if (timer) clearTimeout(timer);
if (immediate && !timer) {
fn.apply(this, args);
}
timer = setTimeout(() => {
if (!immediate) {
fn.apply(this, args);
}
timer = null;
}, delay);
};
debounced.cancel = function () {
clearTimeout(timer);
timer = null;
};
debounced.flush = function (...args) {
debounced.cancel();
fn.apply(this, args);
};
return debounced;
}
1.5 应用场景
// 搜索输入
const searchInput = document.getElementById('search');
searchInput.addEventListener(
'input',
debounce(async (e) => {
const results = await fetchSuggestions(e.target.value);
renderSuggestions(results);
}, 300)
);
// 窗口调整
window.addEventListener(
'resize',
debounce(() => {
recalculateLayout();
}, 250)
);
// 表单自动保存
editor.addEventListener(
'input',
debounce(() => {
saveDraft();
}, 1000)
);
2. 节流(Throttle)
2.1 原理
节流:在指定时间间隔内最多执行一次,不管触发多少次。
触发: ─A─B─C──D──E─F─G─→
执行: ─A───────D───────G→ (固定间隔执行)
2.2 时间戳实现
function throttle(fn, interval) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime >= interval) {
fn.apply(this, args);
lastTime = now;
}
};
}
缺点:最后一次触发如果不在间隔内,会被忽略。
2.3 定时器实现
function throttle(fn, interval) {
let timer = null;
return function (...args) {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, interval);
}
};
}
缺点:第一次触发需要等待 interval 才执行。
2.4 结合实现(首尾都执行)
function throttle(fn, interval) {
let lastTime = 0;
let timer = null;
return function (...args) {
const now = Date.now();
const remaining = interval - (now - lastTime);
if (remaining <= 0) {
if (timer) {
clearTimeout(timer);
timer = null;
}
fn.apply(this, args);
lastTime = now;
} else if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args);
lastTime = Date.now();
timer = null;
}, remaining);
}
};
}
2.5 支持取消
function throttle(fn, interval) {
let lastTime = 0;
let timer = null;
const throttled = function (...args) {
const now = Date.now();
const remaining = interval - (now - lastTime);
if (remaining <= 0) {
if (timer) {
clearTimeout(timer);
timer = null;
}
fn.apply(this, args);
lastTime = now;
} else if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args);
lastTime = Date.now();
timer = null;
}, remaining);
}
};
throttled.cancel = function () {
clearTimeout(timer);
timer = null;
lastTime = 0;
};
return throttled;
}
2.6 应用场景
// 滚动事件
window.addEventListener(
'scroll',
throttle(() => {
updateScrollPosition();
lazyLoadImages();
}, 100)
);
// 鼠标移动
canvas.addEventListener(
'mousemove',
throttle((e) => {
drawTrail(e.clientX, e.clientY);
}, 16)
); // ~60fps
// 按钮防重复点击
submitBtn.addEventListener(
'click',
throttle(async () => {
await submitForm();
}, 2000)
);
3. 对比总结
| 维度 | 防抖(Debounce) | 节流(Throttle) |
|---|---|---|
| 执行时机 | 停止触发后执行 | 固定间隔执行 |
| 触发频率 | 最后一次触发才执行 | 间隔内最多执行一次 |
| 首次执行 | 可配置 immediate | 可配置 leading |
| 末次执行 | 总是执行 | 可配置 trailing |
| 典型场景 | 搜索输入、自动保存 | 滚动、拖拽、resize |
4. requestAnimationFrame 节流
// 适用于动画/视觉更新场景
function rafThrottle(fn) {
let rafId = null;
return function (...args) {
if (rafId !== null) return;
rafId = requestAnimationFrame(() => {
fn.apply(this, args);
rafId = null;
});
};
}
// 使用
element.addEventListener(
'mousemove',
rafThrottle((e) => {
updatePosition(e.clientX, e.clientY);
})
);