在前端开发中,滚动监听、鼠标移动以及窗口resize等事件会在短时间内高频触发,如果每次触发都执行复杂逻辑,容易造成主线程阻塞。节流函数的作用就是限制某个函数在一定时间间隔内最多执行一次,从而平衡响应速度与性能消耗。下面我们通过手写代码来搞清楚它的实现机制。

什么是节流函数
节流(throttle)与防抖(debounce)不同,防抖是把多次触发合并为最后一次触发后执行,而节流是保证在连续触发过程中,每隔一段时间必然执行一次。可以理解为“匀速放行”:无论外界事件多密集,内部逻辑按照固定节奏运行。
从底层看,节流依赖两个要素:上一次执行的时间点,以及当前是否处于等待中的定时器。通过比较时间差或利用定时器状态,决定本次调用是直接执行、忽略还是延后执行。这种机制在动画计算、实时搜索联想等场景非常实用。
基于时间戳的节流实现
时间戳版是最直观的写法。我们记录上一次执行的时间,当新调用进来时,如果距离上次执行已经超过了设定的间隔,就立即执行并更新时间戳;否则什么都不做。
function throttle(fn, wait) {
let previous = 0;
return function() {
const now = Date.now();
const context = this;
const args = arguments;
if (now - previous >= wait) {
fn.apply(context, args);
previous = now;
}
};
}
// 使用示例
window.addEventListener('scroll', throttle(function() {
console.log('滚动事件处理,每秒最多一次');
}, 1000));
这种写法的优点是第一次触发会立刻执行,用户操作能得到迅速反馈。但它的不足在于:如果最后一次触发距离上一次执行不到wait时间,这次调用就会被直接丢弃,可能导致末尾状态没有更新。
例如用户停止滚动时,如果停止前最后一次滚动刚好被忽略,页面的最终位置相关计算就不会跑。所以对于某些需要“收尾”的逻辑,纯时间戳版并不完美。
基于定时器的节流实现
定时器版换了一个思路:当第一次调用时,如果当前没有定时器,就设置一个定时器,在wait之后执行函数并清空定时器标记。这样既保证间隔内只排一次队,也能在尾部触发后补上执行。
function throttle(fn, wait) {
let timer = null;
return function() {
const context = this;
const args = arguments;
if (!timer) {
timer = setTimeout(function() {
fn.apply(context, args);
timer = null;
}, wait);
}
};
}
// 使用示例
const resizeHandler = throttle(function() {
console.log('窗口大小变化处理');
}, 500);
window.addEventListener('resize', resizeHandler);
定时器版的特点是首次触发不会立即执行,而是等待wait毫秒后才执行,但停止触发后那一次一定会跑完。它适合不需要极速首响、但必须保证末次状态落地的场景。
不过它也有缺点:如果持续触发,函数会每个wait周期执行一次,但首次延迟可能让交互感觉“慢半拍”。实际项目中经常需要结合两者优点。
时间戳与定时器结合的完整版
我们可以把前面两种方案融合:用时间戳控制首响,用定时器保证尾响。逻辑是,若时间差达标就立刻执行;若不达标且暂无定时器,则设一个定时器在剩余时间后执行。
function throttle(fn, wait) {
let previous = 0;
let timer = null;
return function() {
const now = Date.now();
const remaining = wait - (now - previous);
const context = this;
const args = arguments;
if (remaining <= 0) {
if (timer) {
clearTimeout(timer);
timer = null;
}
fn.apply(context, args);
previous = now;
} else if (!timer) {
timer = setTimeout(function() {
fn.apply(context, args);
previous = Date.now();
timer = null;
}, remaining);
}
};
}
// 使用示例
const scrollFn = throttle(function(e) {
console.log('既能首响也能尾响的滚动处理');
}, 800);
document.addEventListener('scroll', scrollFn);
在这个版本里,函数既能在高频触发开头立即响应,又能在停止触发后把最后的状态补执行,兼顾了体验与准确。remaining变量动态计算还需等待的时间,避免定时器写死wait造成叠加偏移。
使用节流时要注意wait参数不能过小,否则和没节流差别不大;也不能过大,否则交互迟钝。通常滚动类设100到200毫秒,resize类设300毫秒左右较为合适。
使用场景与注意事项
节流常用于:无限滚动加载、图表实时重绘、游戏循环中的输入采样等。它和防抖的选择依据是,如果你只关心“最终态”用防抖,如果关心“过程态均匀反馈”用节流。
另外在React或Vue等框架中,如果把节流函数写在渲染函数里,每次渲染都会生成新函数导致失效,应当用useRef或模块级变量保存节流后的函数引用,才能保证计时状态连续。
| 实现方式 | 首次是否立即执行 | 末次是否保证执行 | 适用偏好 |
|---|---|---|---|
| 时间戳版 | 是 | 否 | 优先响应 |
| 定时器版 | 否 | 是 | 保证收尾 |
| 结合版 | 是 | 是 | 综合体验 |
通过上述手写过程可以看到,节流并不依赖任何外部库,理解其时间判断与闭包保存状态的原理后,就能针对业务灵活调整。掌握它有助于写出更流畅、更省资源的前端交互代码。
JavaScript节流函数throttle修改时间:2026-08-05 10:03:36