实现一个节流函数

91 阅读10分钟

说在前面

在 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,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『前端也能这么有趣』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。