前言
最近做前端页面时,遇到一个挺有意思的需求:用户调整窗口大小时,页面背景色随机变化。本以为是个简单功能,结果测试时发现:当用户拖动窗口边缘快速调整大小时,背景色疯狂闪烁——1秒内竟变了10几次!这不仅亮瞎了我的钛合金眼,更导致浏览器CPU占用飙升。之前写了一篇关于防抖的博客,今天就讲讲它的好哥们「节流(Throttle)」
今天就用这个案例,带大家从问题出发,通过一段20行的代码,彻底搞懂节流的核心逻辑和实际应用。
问题复现:没有节流的「疯狂变色」
先看最初的代码(去掉节流的版本):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>无节流的背景变色</title>
</head>
<body>
<script>
function coloring() {
// 生成0-255的随机RGB值
const r = Math.floor(Math.random() * 255);
const g = Math.floor(Math.random() * 255);
const b = Math.floor(Math.random() * 255);
// 设置背景色
document.body.style.background = `rgb(${r},${g},${b})`;
}
// 直接绑定resize事件
window.addEventListener('resize', coloring);
</script>
</body>
</html>
打开这个页面,尝试快速拖动窗口边缘调整大小——你会看到背景色像走马灯一样疯狂变化。用浏览器的开发者工具(F12)查看resize
事件的触发频率,发现1秒内竟触发了20-30次!
这是因为resize
事件是典型的高频事件:只要窗口尺寸变化,浏览器就会持续触发该事件。如果直接绑定函数,会导致函数高频执行,造成两个问题:
- 性能问题:频繁修改DOM样式(背景色)会触发重绘,增加浏览器负担
- 用户体验差:背景色闪烁会让用户感到眩晕,尤其是对视觉敏感的人群
节流的核心:给事件加个「频率限制器」
要解决这个问题,关键是控制coloring
函数的执行频率。这时候,节流(Throttle)就派上用场了。节流的核心逻辑是:无论事件触发多频繁,保证目标函数在固定时间内最多执行一次。
举个生活化的例子:你家的热水器——不管你多频繁地开关水龙头(高频事件),热水器只会每隔1秒加热一次(固定频率执行)。这样既保证了水温稳定,又避免了热水器过载。
代码实现:20行写出时间戳版节流函数
理解原理后,我们来自己实现节流函数。用户提供的代码中用了「时间戳版」的节流实现,这是最经典的方式之一:
1. 先看完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>节流版背景变色</title>
</head>
<body>
<script>
// 核心功能:生成随机背景色
function coloring() {
const r = Math.floor(Math.random() * 255);
const g = Math.floor(Math.random() * 255);
const b = Math.floor(Math.random() * 255);
document.body.style.background = `rgb(${r},${g},${b})`;
}
// 节流函数实现(时间戳版)
function throttle(func, delay) {
let prevTime = 0; // 记录上一次执行的时间戳
return function() {
const currentTime = Date.now(); // 当前时间戳
// 如果当前时间与上次执行时间间隔超过delay,执行函数
if (currentTime - prevTime >= delay) {
func(); // 执行目标函数
prevTime = currentTime; // 更新上次执行时间
}
};
}
// 绑定resize事件,使用节流(延迟1000ms)
window.addEventListener('resize', throttle(coloring, 1000));
</script>
</body>
</html>
2. 逐行解析核心代码
-
coloring函数:
负责生成随机RGB值并设置背景色。Math.floor(Math.random()*255)
会生成0-255的整数(包括0,不包括255),确保每个颜色通道值有效。 -
throttle函数:
prevTime
:闭包中保存的变量,记录上一次执行func
的时间戳。初始值为0,保证第一次触发时立即执行。currentTime
:每次事件触发时获取当前时间戳(精确到毫秒)。- 条件判断
currentTime - prevTime >= delay
:如果当前时间与上次执行时间的间隔超过设定的delay
(本例中是1000ms),则执行func
并更新prevTime
。
-
事件绑定:
window.addEventListener('resize', throttle(coloring, 1000))
将resize
事件绑定到节流后的coloring
函数,意味着无论用户多快调整窗口大小,coloring
最多每秒执行一次。
效果对比:用事实说话
为了验证节流效果,我们可以在coloring
函数中添加日志:
function coloring() {
const r = Math.floor(Math.random() * 255);
const g = Math.floor(Math.random() * 255);
const b = Math.floor(Math.random() * 255);
document.body.style.background = `rgb(${r},${g},${b})`;
console.log(`背景色变化,时间:${new Date().toLocaleTimeString()}`);
}
无节流时:
- 快速拖动窗口1秒,控制台会输出20-30条日志(对应20-30次背景色变化)。
- 浏览器的性能面板(Performance标签)显示,
resize
事件处理函数的执行时间占比高达30%以上。
有节流(1000ms)时:
- 同样快速拖动窗口1秒,控制台最多输出1条日志(每秒最多执行一次)。
- 即使持续拖动窗口5秒,控制台也只会输出5条日志(每秒一次)。
- 性能面板显示,
resize
事件处理函数的执行时间占比降至5%以下,页面滚动更流畅。
节流的其他实现方式:时间戳vs定时器
用户提供的代码用了「时间戳版」节流,实际开发中还有另一种常见的「定时器版」实现。我们来对比两者的差异:
1. 时间戳版(上面例子)
function throttle(func, delay) {
let prevTime = 0;
return function() {
const currentTime = Date.now();
if (currentTime - prevTime >= delay) {
func();
prevTime = currentTime;
}
};
}
特点:
- 第一次触发事件时立即执行(因为
prevTime
初始为0,currentTime - 0
肯定大于delay
)。 - 最后一次触发事件后,不会补执行(比如用户停止拖动窗口,不会再触发一次)。
2. 定时器版
function throttle(func, delay) {
let timer = null;
return function() {
if (!timer) {
timer = setTimeout(() => {
func();
timer = null; // 执行后清空定时器
}, delay);
}
};
}
特点:
- 第一次触发事件时,延迟
delay
时间后执行(因为需要等待定时器触发)。 - 最后一次触发事件后,会补执行一次(定时器会在
delay
时间后触发)。
3. 综合版:鱼和熊掌兼得
实际开发中,我们可能希望:第一次触发立即执行,最后一次触发也补执行。这时候可以结合时间戳和定时器:
function throttle(func, delay) {
let prevTime = 0;
let timer = null;
return function() {
const currentTime = Date.now();
// 如果距离上次执行超过delay,立即执行(时间戳逻辑)
if (currentTime - prevTime >= delay) {
// 如果有未完成的定时器,先取消
if (timer) {
clearTimeout(timer);
timer = null;
}
func();
prevTime = currentTime;
}
// 否则设置定时器,保证最后一次触发会执行(定时器逻辑)
else if (!timer) {
timer = setTimeout(() => {
func();
prevTime = Date.now();
timer = null;
}, delay - (currentTime - prevTime)); // 计算剩余等待时间
}
};
}
特点:
- 第一次触发立即执行。
- 中间高频触发时,每隔
delay
时间执行一次。 - 最后一次触发后,会在剩余时间内补执行一次。
节流的应用场景:不止是背景变色
通过这个背景变色的案例,我们能直观感受到节流的价值:将高频事件转化为低频执行,平衡性能与体验。除了resize
事件,节流在以下场景也非常实用:
1. 滚动加载更多
当用户滚动页面时,scroll
事件会高频触发。使用节流可以保证每隔300ms检测一次是否滚动到页面底部,避免频繁请求接口。
2. 游戏技能冷却
游戏中,技能释放后需要冷却时间(比如2秒)。节流可以保证技能在冷却时间内无法重复释放,避免玩家无限连放。
3. 窗口resize计算布局
调整窗口大小时,需要重新计算元素布局(比如网格列数)。使用节流可以避免频繁计算,提升页面流畅度。
总结:节流的核心价值与最佳实践
通过这个背景变色的小实验,我们不仅学会了节流的代码实现,更理解了它的核心价值:在高频事件中,控制目标函数的执行频率,既保证用户体验,又降低性能消耗。
核心总结
- 节流的本质:给高频事件加一个「频率限制器」,固定时间内最多执行一次。
- 时间戳版:立即执行,无收尾补执行;定时器版:延迟执行,有收尾补执行;综合版:两者兼顾。
- 适用场景:需要定期更新状态的高频事件(如
resize
、scroll
、游戏帧更新)。
最佳实践建议
- 合理设置延迟时间:根据业务场景测试调整。
resize
和scroll
通常用100-300ms,游戏技能冷却用1000ms以上。 - 处理事件参数:如果目标函数需要事件对象(如
event.target
),记得用arguments
传递参数(参考function throttle(func, delay) { ... }
的综合版实现)。 - 使用成熟库:如果项目允许,推荐使用
lodash.throttle
,它处理了this
指向、参数传递、取消功能等边缘情况。 - 结合防抖使用:防抖(Debounce)适合「停止操作后执行最终结果」的场景(如搜索输入),节流适合「定期执行」的场景,两者互补。
最后想说,前端开发中,很多看似简单的功能(比如背景变色)背后都隐藏着性能优化的学问。理解节流这样的基础概念,不仅能解决具体问题,更能培养「从用户体验出发,从性能细节入手」的技术思维。
下次遇到高频事件问题时,不妨试试节流——你会发现,控制频率,反而能让交互更流畅。如果本文对你有帮助,欢迎点赞收藏,评论区聊聊你遇到的高频事件优化案例~