Skip to main content

防抖节流

核心区别

-防抖 debounce节流 throttle
触发后行为重新计时,只在「停止触发后」执行一次固定间隔内最多执行一次
关注点等待事件结束控制执行频率
典型场景搜索框输入联想、窗口 resize、表单校验滚动加载、mousemove、按钮防连点

高级面试里,光写出能跑的版本不够,要点在于:this 与参数透传、立即执行(leading)/末次执行(trailing)选项、取消(cancel)能力

防抖(debounce)

/**
* @param {Function} fn
* @param {number} delay
* @param {boolean} immediate 是否在「第一次触发时」立即执行
*/
function debounce(fn, delay = 300, immediate = false) {
let timer = null;

function debounced(...args) {
// 保留 this 指向(如 DOM 事件里 this 指向元素)
const context = this;

if (timer) clearTimeout(timer);

if (immediate) {
// 没有等待中的定时器时,立即执行一次
const callNow = !timer;
timer = setTimeout(() => {
timer = null;
}, delay);
if (callNow) fn.apply(context, args);
} else {
timer = setTimeout(() => {
fn.apply(context, args);
}, delay);
}
}

// 支持取消(如组件卸载时清理)
debounced.cancel = function () {
clearTimeout(timer);
timer = null;
};

return debounced;
}

节流(throttle)

时间戳实现(首次立即执行),逻辑清晰、易写:

function throttle(fn, delay = 300) {
let last = 0;
return function (...args) {
const now = Date.now();
if (now - last >= delay) {
last = now;
fn.apply(this, args);
}
};
}

时间戳 + 定时器结合版:首次立即执行,最后一次也会补上(更接近 lodash 行为):

function throttle(fn, delay = 300) {
let last = 0;
let timer = null;

return function (...args) {
const context = this;
const now = Date.now();
const remaining = delay - (now - last);

if (remaining <= 0) {
// 已超过间隔,立即执行,并清掉可能存在的尾部定时器
if (timer) {
clearTimeout(timer);
timer = null;
}
last = now;
fn.apply(context, args);
} else if (!timer) {
// 还没到时间,安排一次尾部执行,保证最后一次触发不丢
timer = setTimeout(() => {
last = Date.now();
timer = null;
fn.apply(context, args);
}, remaining);
}
};
}

常见追问

  • 防抖和节流底层都用什么实现? setTimeout(节流也可用时间戳),React 里清理要在 useEffect 的返回函数中调用 cancel
  • requestAnimationFrame 能做节流吗? 能,把回调放进 rAF,按屏幕刷新率(约 16.7ms)节流,更适合滚动/动画类场景。
  • lodash 的 debounce/throttle 有哪些选项? leadingtrailingmaxWait(防抖也能保证最长等待时间内必执行一次)。

参考