说在前面
在 JavaScript 编程中,性能优化常常是一个关键的考量因素,尤其是在处理频繁触发的事件时。
节流
就是一种用于控制函数执行频率,避免过度调用导致性能问题的关键方法。今天我们结合力扣原题:2676. 节流 来实现一个节流函数。
需求
现给定一个函数 fn
和一个以毫秒为单位的时间 t
,请你返回该函数的 节流 版本。
节流 函数首先立即被调用,然后在 t
毫秒的时间间隔内不能再次执行,但应该存储最新的函数参数,以便在延迟结束后使用这些参数调用 fn
。
例如,t = 50ms
,并且函数在 30ms
、 40ms
和 60ms
时被调用。
在 30ms
,节流 函数 fn
会以这些函数调用,并且对 节流 函数 fn
的调用在接下来的 t
毫秒会被阻塞。
在 40ms
,函数应当只是存储参数。
在 60ms
,参数应该覆盖第二次调用中当前存储的参数,因为第二次和第三次调用是在 80ms
之前进行的。延迟结束后,应该使用延迟期间提供的最新参数来调用 节流 函数 fn
,并且它还应该创建另一个 80ms + t
的延迟。
上面的图示展示了节流如何转换事件。每个矩形代表100毫秒,节流时间为400毫秒。每种颜色代表不同的输入集合。
示例
示例 1
输入:t = 100,
calls = [
{"t":20,"inputs":[1]}
]
输出:[{"t":20,"inputs":[1]}]
解释:第一次调用总是立即执行,没有延迟。
示例 2
输入:t = 50,
calls = [ {"t":50,"inputs":[1]},
{"t":75,"inputs":[2]}
]
输出:[{"t":50,"inputs":[1]},{"t":100,"inputs":[2]}]
解释:
第一次调用立即执行带有参数 (1) 的函数。
第二次调用发生在 75毫秒 时,在延迟期间内,因为 50毫秒 + 50毫秒 = 100毫秒,所以下一次调用可以在 100毫秒 时执行。因此,我们保存第二次调用的参数,以便在第一次调用的回调函数中使用。
示例 3
输入:t = 70,
calls = [ {"t":50,"inputs":[1]},
{"t":75,"inputs":[2]},
{"t":90,"inputs":[8]},
{"t": 140, "inputs":[5,7]},
{"t": 300, "inputs": [9,4]}
]
输出:[{"t":50,"inputs":[1]},{"t":120,"inputs":[8]},{"t":190,"inputs":[5,7]},{"t":300,"inputs":[9,4]}]
解释:
第一次调用立即执行带有参数 (1) 的函数。
第二次调用发生在 75毫秒 时,在延迟期间内,因为 50毫秒 + 70毫秒 = 120毫秒,所以它只应保存参数。
第三次调用也在延迟期间内,因为我们只需要最新的函数参数,所以覆盖之前的参数。延迟期过后,在 120毫秒时进行回调,并使用保存的参数进行调用。该回调会创建另一个延迟期间,时长为 120毫秒 + 70毫秒 = 190毫秒,以便下一个函数可以在 190毫秒时调用。
第四次调用发生在 140毫秒,在延迟期间内,因此应在190毫秒时作为回调进行调用。这将创建另一个延迟期间,时长为 190毫秒 + 70毫秒 = 260毫秒。
第五次调用发生在 300毫秒,但它是在 260毫秒 之后,所以应立即调用,并创建另一个延迟期间,时长为 300毫秒 + 70毫秒 = 370毫秒。
代码实现
一、函数定义与参数
首先,我们来看这个节流函数的定义:
var throttle = function (fn, t) {
//...
};
它接受两个参数:
fn
:这是一个函数类型的参数,代表需要被节流的目标函数。例如,它可以是像console.log
这样简单的函数,用于打印信息,也可以是在特定业务场景下执行复杂逻辑的自定义函数,比如处理页面滚动事件时的一些操作函数。t
:这是一个数字类型的参数,表示时间间隔(单位为毫秒)。它决定了在多长时间内,目标函数fn
的执行将被限制。
二、函数内部实现原理
在函数内部,首先定义了两个变量:
let lastTimeStamp = -Infinity;
let lastFn = null;
lastTimeStamp
初始化为负无穷大,它用于记录目标函数上一次被允许执行的时间戳。这样的初始化值确保了在函数首次被调用时,不会因为时间戳的比较而被阻止执行。lastFn
初始化为null
,它将用于存储定时器的引用。
然后,函数返回一个新的函数:
return function (...args) {
//...
};
这个新函数使用了剩余参数语法(...args
),以便能够接收任意数量的参数,并将这些参数传递给目标函数 fn
。
在新返回的函数内部,首先执行 clearTimeout(lastFn)
。这一步的目的是清除之前可能存在的定时器。如果在时间间隔 t
内,函数被多次调用,那么每次调用时都先清除之前设置的定时器,以避免不必要的定时器累积和延迟执行。
接着,设置一个新的定时器:
lastFn = setTimeout(
() => {
fn(...args);
lastTimeStamp = new Date().getTime();
},
Math.max(lastTimeStamp + t - new Date().getTime(), 0)
);
定时器的回调函数中,首先执行目标函数 fn
,并传入接收到的参数 ...args
。然后,更新 lastTimeStamp
为当前的时间戳,记录这次函数执行的时间,以便后续的时间间隔判断。定时器的延迟时间通过 Math.max(lastTimeStamp + t - new Date().getTime(), 0)
计算得出。这个计算的目的是确保定时器的延迟时间是合理的。如果当前时间已经超过了上一次执行时间加上时间间隔 t
,那么延迟时间为 0
,即立即执行目标函数;否则,延迟时间为剩余的时间间隔,以保证目标函数在正确的时间间隔后执行。
三、示例说明
以下是一个示例用法:
const throttled = throttle(console.log, 100);
throttled("log"); // logged immediately.
throttled("log"); // logged at t=100ms.
首先,通过 throttle(console.log, 100)
创建了一个经过节流处理的函数 throttled
,这里将 console.log
作为目标函数,时间间隔 t
设置为 100
毫秒。
当第一次调用 throttled("log")
时,由于 lastTimeStamp
初始为负无穷大,所以定时器的延迟时间为 0
,立即执行 console.log("log")
,并更新 lastTimeStamp
为当前时间。
当第二次调用 throttled("log")
时,因为距离上一次执行的时间间隔还在 100
毫秒内,所以会设置一个新的定时器,其延迟时间为剩余的时间间隔,即大约 100
毫秒减去已经过去的时间。当这个定时器触发时,才会再次执行 console.log("log")
。
通过这种方式,节流函数有效地控制了目标函数在指定时间间隔内的执行次数,避免了函数因频繁触发事件而被过度调用,从而提高了程序的性能和稳定性,特别是在处理诸如窗口滚动事件、鼠标移动事件等频繁触发的场景中具有重要的应用价值。
四、完整代码
/**
* @param {Function} fn
* @param {number} t
* @return {Function}
*/
const throttle = function (fn, t) {
let lastTimeStamp = -Infinity;
let lastFn = null;
return function (...args) {
clearTimeout(lastFn);
lastFn = setTimeout(
() => {
fn(...args);
lastTimeStamp = new Date().getTime();
},
Math.max(lastTimeStamp + t - new Date().getTime(), 0)
);
};
};
/**
* const throttled = throttle(console.log, 100);
* throttled("log"); // logged immediately.
* throttled("log"); // logged at t=100ms.
*/
实际应用场景
1.滚动事件节流 - 固定导航栏效果增强
- 场景描述:在网页开发中,当页面内容较多且有一个固定在顶部的导航栏时,通常希望在用户向下滚动一定距离后改变导航栏的样式(如改变背景颜色、显示或隐藏某些元素)。滚动事件会频繁触发,没有节流可能导致性能问题。
- 代码示例:
const changeNavbarStyle = function () {
const scrollPosition = window.scrollY;
if (scrollPosition > 100) {
document.getElementById('navbar').style.backgroundColor = 'rgba(0,0,0,0.8)';
} else {
document.getElementById('navbar').style.backgroundColor = 'transparent';
}
};
const throttledChangeNavbarStyle = throttle(changeNavbarStyle, 200);
window.addEventListener('scroll', throttledChangeNavbarStyle);
- 解释:当用户滚动窗口时,
throttle
函数会确保changeNavbarStyle
函数最多每200毫秒执行一次。这样可以避免频繁地改变导航栏样式,提高页面性能,同时也能给用户一个流畅的视觉体验。
2.鼠标移动事件节流 - 自定义鼠标轨迹特效优化
- 场景描述:在一些具有创意的网页设计中,可能会根据鼠标的移动轨迹来绘制线条或者生成特效。如果鼠标移动事件不进行节流,会导致线条绘制过于密集或者特效过度触发,消耗大量资源。
- 代码示例:
const drawMouseTrail = function (e) {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
ctx.beginPath();
ctx.arc(e.clientX, e.clientY, 5, 0, 2 * Math.PI);
ctx.fillStyle = 'blue';
ctx.fill();
};
const throttledDrawMouseTrail = throttle(drawMouseTrail, 30);
document.addEventListener('mousemove', throttledDrawMouseTrail);
- 解释:在这个例子中,当用户移动鼠标时,
drawMouseTrail
函数用于在一个canvas
元素上绘制蓝色的圆形来表示鼠标轨迹。通过throttle
函数限制该函数最多每30毫秒执行一次,使得绘制的鼠标轨迹既能够实时地反映鼠标的大致移动路径,又不会因为过于频繁的绘制而导致页面卡顿。
3.搜索框输入事件节流 - 高效的搜索建议显示
- 场景描述:在带有搜索功能的应用或网站中,当用户在搜索框中输入内容时,通常会实时显示搜索建议。如果每次用户输入一个字符都立即请求搜索建议,会给服务器带来很大压力,并且可能因为频繁的网络请求导致搜索建议的显示不及时或不稳定。
- 代码示例:
const getSearchSuggestions = function (inputValue) {
// 这里假设通过fetch API向服务器发送请求获取搜索建议
console.log(`正在获取包含${inputValue}的搜索建议...`);
// 实际应用中,这里应该是发送请求并处理返回的建议
};
const throttledGetSearchSuggestions = throttle(getSearchSuggestions, 500);
const searchInput = document.getElementById('search-input');
searchInput.addEventListener('input', function () {
throttledGetSearchSuggestions(this.value);
});
- 解释:当用户在搜索框中输入内容时,
throttle
函数会确保getSearchSuggestions
函数最多每500毫秒执行一次。这样可以合理地控制向服务器发送搜索建议请求的频率,减轻服务器负担,同时也能让用户在一个相对合理的时间内看到搜索建议。
公众号
关注公众号『前端也能这么有趣
』,获取更多有趣内容。
说在后面
🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『
前端也能这么有趣
』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。