功能说明
该示例实现节流函数:在固定时间窗内最多执行一次目标函数。leading 控制第一次调用是否立即执行,trailing 控制窗口结束时是否用最后一次参数补一次调用。它适合滚动监听、拖拽反馈、搜索联想等高频事件,避免回调堆积。
关键代码
throttled 记录 started、lastInvoke 与待处理参数。首次调用根据 leading 决定立即执行还是排入尾部定时器。之后计算距上次执行的时间差,超过 wait 就取消旧定时器并立即执行;否则在 trailing 开启时保存最新参数并调度剩余时间的定时器。invoke 统一清空待处理参数并更新时间戳。cancel 清理定时器和状态,flush 立即执行挂起的尾调用。
使用注意
尾调用只使用最后一次参数,中间参数会被丢弃。函数通过 apply 保留调用时的 this,但箭头函数本身没有自己的 this。wait 为 0 时每次调用都会立即执行。它不是分布式限流器,多实例部署仍需 Redis 或网关限流。组件卸载时应调用 cancel,避免定时器在销毁后继续执行。异步函数被节流时,返回值为 undefined,调用方拿不到原始 Promise。
验证方式
用假定时器连续调用三次,断言 leading 只执行一次,推进时间后 trailing 用最后参数执行一次。测试 leading 为 false 时首次不执行、窗口结束才执行。调用 cancel 后推进时间断言不再执行,调用 flush 断言挂起调用立即执行。最后验证非法 fn、负数 wait 与 this 绑定。
代码示例
export function throttle(fn, wait, options = {}) {
if (typeof fn !== "function") throw new TypeError("fn must be a function");
if (!Number.isFinite(wait) || wait < 0) throw new RangeError("wait must be a non-negative number");
const { leading = true, trailing = true } = options;
let timer = null;
let lastInvoke = 0;
let pendingArgs = null;
let pendingThis = null;
let started = false;
function invoke() {
const args = pendingArgs;
const context = pendingThis;
pendingArgs = null;
pendingThis = null;
if (args === null) return;
lastInvoke = Date.now();
fn.apply(context, args);
}
function schedule(delay) {
if (timer !== null) return;
timer = setTimeout(() => {
timer = null;
if (trailing) invoke();
}, delay);
}
function throttled(...args) {
const now = Date.now();
if (!started) {
started = true;
lastInvoke = now;
if (leading) {
fn.apply(this, args);
} else {
pendingArgs = args;
pendingThis = this;
schedule(wait);
}
return;
}
const elapsed = now - lastInvoke;
if (elapsed >= wait) {
if (timer !== null) {
clearTimeout(timer);
timer = null;
}
lastInvoke = now;
pendingArgs = null;
pendingThis = null;
fn.apply(this, args);
return;
}
if (trailing) {
pendingArgs = args;
pendingThis = this;
schedule(wait - elapsed);
}
}
throttled.cancel = () => {
if (timer !== null) clearTimeout(timer);
timer = null;
pendingArgs = null;
pendingThis = null;
started = false;
lastInvoke = 0;
};
throttled.flush = () => {
if (timer !== null) {
clearTimeout(timer);
timer = null;
}
invoke();
};
return throttled;
}
const send = throttle((value) => console.log("sent", value), 200, {
leading: true,
trailing: true,
});
send(1);
send(2);
setTimeout(() => send(3), 250);