js 防抖节流是什么怎么写
说明防抖与节流的区别及适用场景(防抖取最终结果、节流保证过程持续反馈),并给出基于闭包和定时器的实现代码,附延迟取值、立即执行、cancel 清理与首尾执行等注意事项。
防抖(debounce)和节流(throttle)都是用闭包和定时器控制函数执行频率的手段:防抖是“等你停下来再执行”,事件停止触发 n 毫秒后才执行一次;节流是“固定频率执行”,无论触发多密集,每 n 毫秒最多执行一次。搜索框输入用防抖,滚动、拖拽用节流。
防抖和节流有什么区别?
结论:防抖只保留最后一次触发,节流保证单位时间内都有执行,选错会导致功能表现完全相反。
- 防抖:连续触发 10 次,只在停止后执行 1 次。典型场景是搜索联想输入(用户敲完字才发请求)、窗口 resize 结束再重算布局、表单提交按钮防止重复点击。
- 节流:连续触发 10 次,在 1 秒窗口内可能执行 2-3 次,频率可控。典型场景是 scroll 监听、mousemove 拖拽、无限滚动加载。
一句话判断:如果你只关心“最终结果”,用防抖;如果过程需要持续反馈(进度、位置、懒加载),用节流。
防抖怎么写?
结论:防抖的核心是每次触发都清掉上一个定时器再重设,同时用 apply 保住 this 和参数。
function debounce(fn, delay = 300, immediate = false) {
let timer = null;
function debounced(...args) {
if (timer) clearTimeout(timer);
if (immediate && !timer) {
fn.apply(this, args); // 首次立即执行
}
timer = setTimeout(() => {
timer = null;
if (!immediate) fn.apply(this, args);
}, delay);
}
debounced.cancel = () => { clearTimeout(timer); timer = null; };
return debounced;
}
三个必须注意的点:
delay取值:搜索框输入建议 300ms,按钮防重复提交建议 500-1000ms,resize 建议 200ms。低于 100ms 基本起不到降低请求量的作用。immediate开关决定首次是否立即执行,按钮点击场景通常要设成true,否则用户会感觉“点了没反应”。- 必须暴露
cancel。组件卸载或在useEffect的 cleanup 里调用cancel(),否则定时器持有闭包引用,会造成内存泄漏,并在卸载后仍触发一次回调报错。
节流怎么写?
结论:节流有两种写法——时间戳版(首次立即执行、末次不执行)和定时器版(首次延迟执行、末次会补一次),生产环境建议用“首尾都执行”的组合版。
时间戳版,适合滚动中需要即时反馈的场景:
function throttle(fn, interval = 200) {
let last = 0;
return function (...args) {
const now = Date.now();
if (now - last >= interval) {
last = now;
fn.apply(this, args);
}
};
}
定时器版,适合需要保证最后一次触发的场景(比如滚动到底部加载更多):
function throttle(fn, interval = 200) {
let timer = null;
return function (...args) {
if (timer) return;
timer = setTimeout(() => {
timer = null;
fn.apply(this, args);
}, interval);
};
}
interval 的取值参考:scroll 用 100-200ms,mousemove 用 50-100ms,按钮点击用 500ms。低于 50ms 对用户来说已接近连续,节流收益很小。
实际项目里还要不要手写?
结论:业务代码优先直接用 lodash 的 debounce / throttle,它们已经处理了 this、参数透传、cancel()、flush() 和 leading / trailing 配置;手写版本的价值在于理解原理和应对不能引第三方库的场景。
lodash.debounce(fn, 300, { leading: true, trailing: false }) 对应“首次立即执行、停止后不再执行”;lodash.throttle(fn, 200, { leading: true, trailing: true }) 对应“首尾都执行”。
框架里的两个常见问题:
- Vue:在
onUnmounted里调用debounced.cancel(),否则路由切换后定时器仍会执行并访问已销毁的响应式数据。 - React:不要在渲染函数里直接调用
debounce(fn, 300)生成新函数,每次渲染都会产生新实例,防抖完全失效。正确做法是useMemo/useRef缓存,或把fn用useCallback包好再进useMemo依赖。
总结一下:防抖靠“清定时器 + 重设”实现末次执行,节流靠“时间戳差”或“定时器占位”实现固定频率;延时按场景取 50-1000ms,this 用 apply 保住,卸载时记得 cancel;能用 lodash 就别手写,手写的意义是知道它内部发生了什么。
转载请注明出处,版权归原作者所有。