节流函数默认不执行最后一次调用,因其仅判断是否到达执行周期而无兜底机制;支持trailing需维护待执行状态并用setTimeout在延迟后补调用。

节流函数默认不保证“时间窗口结束后执行最后一次”,这是它和防抖的关键区别之一。但可以通过添加 trailing(尾部执行)选项来实现这个行为——即:在连续触发期间,按固定间隔执行;若最后一次触发后没到下一个间隔点,就在时间窗口“冷却结束”时补上一次。
为什么默认不执行最后一次?
基础节流(如时间戳版或简单 setTimeout 版)只关注“是否到了执行周期”,不记录“有没有遗漏的调用”。比如 500ms 节流,用户在 490ms 触发后立刻停止,那这次调用就被跳过了——因为还没到 500ms 的节点,且没有机制去兜底。
如何支持“窗口结束后补执行”?
关键是在节流内部维护两个状态:是否正在等待执行(定时器),以及是否“有未处理的调用待执行”。常用做法是:
- 每次触发时,缓存 this 上下文 和 参数
- 若无活跃定时器,则启动一个延迟执行;否则仅更新缓存
- 在定时器回调中执行函数,并清空缓存、重置定时器 ID
- 提供 cancel() 方法可手动清除待执行任务
带 trailing 的完整实现示例
以下是一个支持 leading(首次立即执行)、trailing(末次补执行)和 cancel 的节流函数:
function throttle(func, delay, options = {}) {
const { leading = true, trailing = true } = options;
let timeoutId = null;
let lastArgs = null;
let lastThis = null;
let lastCallTime = 0;
<p>const invokeFunc = () => {
func.apply(lastThis, lastArgs);
lastArgs = null;
lastThis = null;
};</p><p>const startTimer = () => {
timeoutId = setTimeout(() => {
if (trailing && lastArgs !== null) {
invokeFunc();
}
timeoutId = null;
}, delay);
};</p><p>const throttled = function(...args) {
lastThis = this;
lastArgs = args;
const now = Date.now();</p><pre class='brush:php;toolbar:false;'>if (!timeoutId) {
if (leading && now - lastCallTime >= delay) {
invokeFunc();
lastCallTime = now;
} else {
startTimer();
}
}};
throttled.cancel = () => { if (timeoutId !== null) { clearTimeout(timeoutId); timeoutId = null; lastArgs = null; lastThis = null; } };
return throttled; }
使用方式:
const handleScroll = throttle(() => {
console.log('滚动处理');
}, 300, { leading: true, trailing: true });
<p>window.addEventListener('scroll', handleScroll);</p>这样,只要滚动停在某个时刻,且距离上次执行已超 300ms,就会在定时器到期时补上一次;如果一直滚动,则严格每 300ms 执行一次。
注意事项
- trailing 依赖定时器:必须用 setTimeout 实现,时间戳版无法自然支持补执行
- 避免重复执行:leading 和 trailing 同时为 true 时,若第一次触发立刻执行,紧接着又在 delay 后补一次,可能造成“同一事件被处理两次”,需根据业务判断是否开启组合
- 内存与上下文安全:务必在执行后清空 lastArgs/lastThis,防止闭包持有大对象或过期 DOM 引用
不复杂但容易忽略细节,关键是把“是否有待执行任务”显式管理起来。


















