约定:
leading: true
--- 在节流/防抖开始时立即触发一次(开头触发)。trailing: true
--- 在等待时间结束后触发一次(尾部触发)。- 默认
trailing = true
,leading = false
(与 lodash 默认一致)。
1) debounce(防抖 --- 支持 leading/trailing)
ini
function debounce(fn, wait = 0, options = {}) {
let timer = null;
let lastArgs = null;
let lastThis = null;
let result;
const leading = !!options.leading;
const trailing = options.trailing !== false; // 默认 true
const invoke = () => {
result = fn.apply(lastThis, lastArgs);
lastArgs = lastThis = null;
};
const startTimer = () => {
timer = setTimeout(() => {
timer = null;
// 如果尾部允许并且有待执行的参数,则执行一次
if (trailing && lastArgs) {
invoke();
} else {
// 如果没有尾调用,清空缓存参数
lastArgs = lastThis = null;
}
}, wait);
};
const debounced = function(...args) {
lastArgs = args;
lastThis = this;
// 若没有定时器,说明是新一段触发
if (!timer) {
if (leading) {
// 立即触发(leading)
invoke();
}
// 无论是否 leading,都要启一个定时器用于阻断下一次 leading(并决定是否执行 trailing)
startTimer();
} else {
// 已有定时器,重置等待时间(以便实现防抖的"延后"语义)
clearTimeout(timer);
startTimer();
}
return result;
};
debounced.cancel = function() {
if (timer) {
clearTimeout(timer);
timer = null;
}
lastArgs = lastThis = null;
};
debounced.flush = function() {
// 立即触发尾部(如果存在)
if (timer) {
clearTimeout(timer);
timer = null;
if (lastArgs && trailing) invoke();
}
return result;
};
return debounced;
}
行为说明(常见组合)
leading: false, trailing: true
(默认)
-> 仅在停止触发后执行一次(普通防抖)。leading: true, trailing: false
-> 立即执行一次,之后在wait
时间内忽略所有调用(只有开头触发)。leading: true, trailing: true
-> 开头立即执行一次;如果在wait
内还发生调用,则在wait
结束后再执行一次(尾部触发传入的最后一次参数)。
使用示例
javascript
const deb = debounce((...args) => console.log('run', ...args), 200, { leading: true, trailing: true });
deb(1); // 立即打印 run 1
deb(2); // 不立即打印,200ms后打印 run 2(尾部)
2) throttle(节流 --- 支持 leading/trailing)
实现思路:维护上次实际触发时间 lastInvokeTime
与一个尾部计时器 timer
。使用 Date.now()
计算剩余时间。
ini
function throttle(fn, wait = 0, options = {}) {
let timer = null;
let lastArgs = null;
let lastThis = null;
let lastInvokeTime = 0; // 上次实际执行的时间
let result;
const leading = options.leading !== false; // 默认 true
const trailing = options.trailing !== false; // 默认 true
const now = () => Date.now();
const invoke = () => {
lastInvokeTime = now();
result = fn.apply(lastThis, lastArgs);
lastArgs = lastThis = null;
};
const remaining = () => wait - (now() - lastInvokeTime);
const throttled = function(...args) {
lastArgs = args;
lastThis = this;
const timeLeft = remaining();
// 一开始如果 lastInvokeTime == 0 && leading == false,我们需要设定 lastInvokeTime
if (lastInvokeTime === 0 && !leading) {
lastInvokeTime = now();
}
if (timeLeft <= 0 || timeLeft > wait) {
// 到达可以立即触发的时刻
if (timer) {
clearTimeout(timer);
timer = null;
}
invoke();
} else if (!timer && trailing) {
// 安排一次尾部触发(在剩余时间后)
timer = setTimeout(() => {
timer = null;
invoke();
}, timeLeft);
}
return result;
};
throttled.cancel = function() {
if (timer) {
clearTimeout(timer);
timer = null;
}
lastArgs = lastThis = null;
lastInvokeTime = 0;
};
throttled.flush = function() {
if (timer) {
clearTimeout(timer);
timer = null;
invoke();
} else if (lastArgs && (now() - lastInvokeTime >= wait)) {
// 如果没有 timer,但满足触发条件
invoke();
}
return result;
};
return throttled;
}
行为说明(常见组合)
leading: true, trailing: false
(常见)
-> 第一次调用立即执行,然后在wait
时间段内忽略后续调用。leading: false, trailing: true
-> 在首次触发后延迟wait
时间执行(第一次不是立即触发),之后若再触发,会再次在时间窗结束时触发。leading: true, trailing: true
-> 开头立即执行;在时间窗内若调用发生,保证在时间窗结束时执行一次(尾部)以处理最后一次调用。
使用示例
javascript
const thr = throttle((...args) => console.log('throttle', ...args), 1000, { leading: true, trailing: true });
thr(1); // 立即打印 1
thr(2); // 忽略立即打印,但在 1s 后会打印 2(如果在 wait 期间有调用)
额外说明、注意点与面试要点
-
this
与 参数 :实现里用lastThis = this
与lastArgs = args
存储上下文与参数,确保最终执行时fn
的this
指向正确,且使用最后一次调用的参数(常见语义)。 -
leading + trailing 组合:
- 如果
leading
为true
,第一次调用会立即触发(如果没有 timer)。 - 为了避免在
wait
内再次立即触发,我们通常在第一次调用后设置一个阻断 timer。 - 如果同时
trailing
为true
,还需要在 timer 到期时根据是否有"最后一次调用参数"来决定是否再触发一次。
- 如果
-
cancel / flush:
cancel()
用于清除等待与参数(中断),常用于组件卸载时清理。flush()
用于立刻触发尾部(若存在),常用于需要把延后任务立即执行的场景。
-
实现上的小坑:
- 不能在
leading
执行后立即把lastArgs
清空------要保留lastArgs
以便尾部触发时使用(若trailing
为 true)。 - 时间计数需要考虑
Date.now
的稳定性(在高精度场景可用performance.now()
)。 - JS 的
setTimeout
精度与事件循环会有延迟,不保证精确到毫秒。
- 不能在
-
测试建议 :写几个场景用例手动测试四种组合(leading/trailing 的 true/false),并测试
cancel
与flush
行为。- 例如:
leading:false,trailing:true
(普通防抖)对快速连续调用只在最后一次触发; leading:true,trailing:false
对开始触发并在等待期间忽略其他调用。
- 例如: