文章目录
前言
在前端开发中,我们经常需要处理一些高频触发但只需定期响应的事件,比如:
- 用户滚动页面(
scroll) - 鼠标移动(
mousemove) - 窗口缩放(
resize) - 游戏中的按键监听
如果不加以控制,这些事件可能在 1 秒内触发上百次,导致:
- 页面重绘/重排频繁
- 动画卡顿、性能下降
- 接口请求过多,服务器压力大
这时,节流(Throttle) 就派上了用场。
本文将带你从 定义、原理、实现过程、使用场景到注意事项,全面掌握 JavaScript 节流机制,与上一篇《防抖》形成完整对比,助你彻底搞懂函数限频优化。
一、什么是节流?(Definition)
定义
节流(Throttle) 是一种函数优化技术,它的核心思想是:无论事件触发多频繁,函数在指定时间周期内最多只执行一次。
换句话说:
“我不可能每次都响应你,但我保证每隔一段时间就执行一次。”
类比理解
想象你坐地铁,每 5 分钟发一班车。
即使你在 5 分钟内多次“请求”地铁,它也只会在 固定时间点 发车。
- 地铁发车 = 执行函数
- 乘客上车 = 事件触发
- 5 分钟间隔 = 节流的等待时间(
wait)
节流就是:固定频率执行,不因触发频繁而加速。
二、节流的原理(Principle)
节流的实现依赖于 JavaScript 的两个核心机制:
- 闭包(Closure):保存上一次执行的时间戳或定时器状态
- 时间判断或定时器:控制函数是否可以执行
执行流程图(时间戳版本)
事件触发
↓
获取当前时间 now
↓
计算距离上次执行的时间差 diff = now - previous
↓
如果 diff >= wait,则执行函数,并更新 previous = now
↓
否则,不执行,等待下一次触发
执行流程图(定时器版本)
事件触发
↓
如果当前没有定时器(timer 为 null)
↓
设置定时器,wait 毫秒后执行函数,并清空 timer
↓
如果已有定时器,则不设置,等待其执行
三、手写节流函数(Implementation)
基础版本(时间戳实现)
/**
* 节流函数(时间戳版本):在 wait 时间内最多执行一次
* @param {Function} func - 要节流的函数
* @param {number} wait - 等待时间(毫秒)
* @returns {Function} - 包装后的节流函数
*/
function throttle(func, wait) {
let previous = 0; // 上一次执行的时间戳
return function (...args) {
const context = this;
const now = Date.now();
if (now - previous >= wait) {
func.apply(context, args);
previous = now;
}
};
}
进阶版本(定时器实现)
/**
* 节流函数(定时器版本):保证最后一次触发也能执行
* @param {Function} func - 要节流的函数
* @param {number} wait - 等待时间(毫秒)
* @returns {Function} - 包装后的节流函数
*/
function throttle(func, wait) {
let timeout = null;
return function (...args) {
const context = this;
if (!timeout) {
timeout = setTimeout(() => {
func.apply(context, args);
timeout = null;
}, wait);
}
};
}
完整版本(双剑合璧:时间戳 + 定时器)
/**
* 节流函数(最终版):首次立即执行,末次也保证执行
* @param {Function} func - 要节流的函数
* @param {number} wait - 等待时间(毫秒)
* @returns {Function} - 包装后的节流函数
*/
function throttle(func, wait) {
let previous = 0;
let timeout = null;
const later = function (...args) {
previous = Date.now();
timeout = null;
func.apply(this, args);
};
return function (...args) {
const context = this;
const now = Date.now();
const remaining = wait - (now - previous);
if (remaining <= 0 || remaining > wait) {
// 如果可以执行,清除定时器,立即执行
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(context, args);
} else if (!timeout) {
// 否则,设置定时器,保证末次执行
timeout = setTimeout(later.bind(context, ...args), remaining);
}
};
}
参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
func | Function | 要节流的函数 |
wait | Number | 等待时间(毫秒) |
四、使用场景与示例
1. 页面滚动事件(监听滚动位置)
const handleScroll = throttle(() => {
console.log('当前滚动位置:', window.scrollY);
// 可用于:吸顶导航、懒加载、滚动动画
}, 100);
window.addEventListener('scroll', handleScroll);
效果:每 100ms 最多执行一次,避免频繁计算。
2. 鼠标移动事件(绘制轨迹)
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let points = [];
const drawPoint = throttle((x, y) => {
points.push({ x, y });
ctx.fillRect(x, y, 2, 2);
}, 16); // 约 60fps
canvas.addEventListener('mousemove', (e) => {
drawPoint(e.clientX, e.clientY);
});
避免绘制过多点,影响性能。
3. 游戏按键监听
const shoot = throttle(() => {
console.log('发射子弹!');
}, 500); // 每 500ms 最多发射一次
document.addEventListener('keydown', (e) => {
if (e.key === ' ') {
shoot();
}
});
防止玩家“连点”过快,影响游戏平衡。
五、注意事项与最佳实践
1. 选择合适的实现方式
| 版本 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 时间戳版 | 首次立即执行 | 停止触发后,最后一次可能不执行 | 首次必须响应 |
| 定时器版 | 保证末次执行 | 首次有延迟 | 末次必须响应 |
| 双剑合璧版 | 首次立即 + 末次保证 | 代码稍复杂 | 推荐使用 |
推荐使用“双剑合璧”版本,兼顾用户体验。
2. this 指向问题
- 使用
apply或call绑定原始this - 箭头函数会改变
this,慎用
3. 与防抖(Debounce)的区别
| 特性 | 节流(Throttle) | 防抖(Debounce) |
|---|---|---|
| 执行时机 | 固定频率执行 | 最后一次触发后执行 |
| 适用场景 | 滚动、鼠标移动、游戏 | 搜索、提交、resize |
| 触发频率 | 至少执行一次 | 可能一次都不执行 |
简单记:节流是“每隔一段时间”,防抖是“最后一次”。
4. 不要滥用节流
- 节流适用于“定期响应”的场景
- 不适用于“必须立即响应”的场景(如按钮点击)
六、React/Vue 中的节流实践
Vue 3 + Composition API
import { onMounted, onUnmounted } from 'vue';
export default {
setup() {
const handleScroll = throttle(() => {
console.log('滚动中...');
}, 100);
onMounted(() => {
window.addEventListener('scroll', handleScroll);
});
onUnmounted(() => {
window.removeEventListener('scroll', handleScroll);
});
return {};
}
}
React + useCallback
import { useCallback, useEffect } from 'react';
function ScrollComponent() {
const throttledScroll = useCallback(
throttle(() => {
console.log('滚动位置:', window.scrollY);
}, 100),
[]
);
useEffect(() => {
window.addEventListener('scroll', throttledScroll);
return () => {
window.removeEventListener('scroll', throttledScroll);
};
}, [throttledScroll]);
return <div style={{ height: '200vh' }}>滚动我</div>;
}
总结
| 要点 | 说明 |
|---|---|
| 核心思想 | 固定频率执行,最多一次 |
| 实现机制 | 时间戳判断 或 定时器控制 |
| 关键技巧 | 保存时间戳、处理末次执行、绑定 this |
| 适用场景 | 滚动、鼠标移动、游戏、动画 |
| 注意事项 | 区分防抖、避免滥用、选择合适版本 |
758

被折叠的 条评论
为什么被折叠?



