回到课程

节流装饰器

重要程度: 5

创建一个“节流”装饰器 throttle(f, ms) —— 返回一个包装器。

当被多次调用时,它会在每 ms 毫秒最多将调用传递给 f 一次。

与防抖(debounce)装饰器相比,其行为完全不同:

  • debounce 会在“冷却(cooldown)”期后运行函数一次。适用于处理最终结果。
  • throttle 运行函数的频率不会大于所给定的时间 ms 毫秒。适用于不应该经常进行的定期更新。

换句话说,throttle 就像接电话的秘书,但是打扰老板(实际调用 f)的频率不能超过每 ms 毫秒一次。

让我们看看现实生活中的应用程序,以便更好地理解这个需求,并了解它的来源。

例如,我们想要跟踪鼠标移动。

在浏览器中,我们可以设置一个函数,使其在每次鼠标移动时运行,并获取鼠标移动时的指针位置。在使用鼠标的过程中,此函数通常会执行地非常频繁,大概每秒 100 次(每 10 毫秒)。

我们想要在鼠标指针移动时,更新网页上的某些信息。

……但是更新函数 update() 太重了,无法在每个微小移动上都执行。高于每 100ms 更新一次的更新频次也没有意义。

因此,我们将其包装到装饰器中:使用 throttle(update, 100) 作为在每次鼠标移动时运行的函数,而不是原始的 update()。装饰器会被频繁地调用,但是最多每 100ms 将调用转发给 update() 一次。

在视觉上,它看起来像这样:

  1. 对于第一个鼠标移动,装饰的变体立即将调用传递给 update。这很重要,用户会立即看到我们对其动作的反应。
  2. 然后,随着鼠标移动,直到 100ms 没有任何反应。装饰的变体忽略了调用。
  3. 100ms 结束时 —— 最后一个坐标又发生了一次 update
  4. 然后,最后,鼠标停在某处。装饰的变体会等到 100ms 到期,然后用最后一个坐标运行一次 update。因此,非常重要的是,处理最终的鼠标坐标。

一个代码示例:

function f(a) {
  console.log(a);
}

// f1000 最多每 1000ms 将调用传递给 f 一次
let f1000 = throttle(f, 1000);

f1000(1); // 显示 1
f1000(2); // (节流,尚未到 1000ms)
f1000(3); // (节流,尚未到 1000ms)

// 当 1000ms 时间到...
// ...输出 3,中间值 2 被忽略

P.S. 参数(arguments)和传递给 f1000 的上下文 this 应该被传递给原始的 f

打开带有测试的沙箱。

function throttle(func, ms) {

  let isThrottled = false,
    savedArgs,
    savedThis;

  function wrapper() {

    if (isThrottled) { // (2)
      savedArgs = arguments;
      savedThis = this;
      return;
    }
    isThrottled = true;

    func.apply(this, arguments); // (1)

    setTimeout(function() {
      isThrottled = false; // (3)
      if (savedArgs) {
        wrapper.apply(savedThis, savedArgs);
        savedArgs = savedThis = null;
      }
    }, ms);
  }

  return wrapper;
}

调用 throttle(func, ms) 返回 wrapper

  1. 在第一次调用期间,wrapper 只运行 func 并设置冷却状态(isThrottled = true)。
  2. 在冷却状态下,所有调用都被保存在 savedArgs/savedThis 中。请注意,上下文(this)和参数(arguments)都很重要,应该被保存下来。我们需要它们来重现调用。
  3. 经过 ms 毫秒后,setTimeout中的函数被触发。冷却状态被移除(isThrottled = false),如果存在被忽略的调用,将使用最后一次调用保存的参数和上下文运行 wrapper

第 3 步运行的不是 func,而是 wrapper,因为我们不仅需要执行 func,还需要再次进入冷却状态并设置 setTimeout 以重置节流。

使用沙箱的测试功能打开解决方案。