回到课程

节流装饰器

重要程度: 5

创建一个“节流”装饰器 throttle(f, ms) —— 返回一个包装器,每隔 “ms” 毫秒将调用最多传递给 f 一次。那些属于“冷却”时期的调用被忽略了。

debounce 的区别 —— 如果被忽略的调用是冷却期间的最后一次,那么它会在延迟结束时执行。

让我们检查一下真实应用程序,以便更好地理解这个需求,并了解它的来源。

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

在浏览器中,我们可以设置一个函数,鼠标的每次微小的运动都执行,并在移动时获取指针位置。在活动鼠标使用期间,此功能通常非常频繁地运行,可以是每秒 100 次(每 10 毫秒)。

跟踪功能应更新网页上的一些信息。

更新函数 update() 太重了,无法在每次微小动作上执行。每 100 毫秒更频繁地制作一次也没有任何意义。

因此我们将 throttle(update, 100) 指定为在每次鼠标移动时运行的函数,而不是原始的 update()。装饰器将经常调用,但 update() 最多每 100ms 调用一次。

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

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

一个代码示例:

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

// f1000 passes calls to f at maximum once per 1000 ms
let f1000 = throttle(f, 1000);

f1000(1); // shows 1
f1000(2); // (throttling, 1000ms not out yet)
f1000(3); // (throttling, 1000ms not out yet)

// when 1000 ms time out...
// ...outputs 3, intermediate value 2 was ignored

附:参数和传递给 f1000 的上下文 this 应该传递给原始的 f

打开带有测试的沙箱。

function throttle(func, ms) {

  let isThrottled = false,
    savedArgs,
    savedThis;

  function wrapper() {

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

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

    isThrottled = true;

    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 中。请注意,上下文和参数都同样重要,应该记住。我们需要他们同时重现这个调用。
  3. …然后在 ms 毫秒过后,setTimeout 触发。冷却状态被删除 (isThrottled = false)。如果我们忽略了调用,则使用最后记忆的参数和上下文执行 wrapper

第3步不是 func,而是 wrapper,因为我们不仅需要执行 func,而是再次进入冷却状态并设置超时以重置它。

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