在 JavaScript 领域里常用的装饰者(decorators)

554 阅读6分钟
通过上篇装饰者模式,call/apply的基础上扩展几个例子

间谍装饰者

重要程度: *****

创建一个装饰者 spy(func),它应该返回一个包装器,该包装器将所有对函数的调用保存在其 calls 属性中。

每个调用都保存为一个参数数组。

例如:

function work(a, b) {
  alert( a + b ); // work 是一个任意的函数或方法
}

work = spy(work);

work(1, 2); // 3
work(4, 5); // 9

for (let args of work.calls) {
  alert( 'call:' + args.join() ); // "call:1,2", "call:4,5"
}

P.S. 该装饰者有时对于单元测试很有用。它的高级形式是 Sinon.JS 库中的 sinon.spy

解决方案:

spy(f) 返回的包装器应存储所有参数,然后使用 f.apply 转发调用。

function spy(func) {

  function wrapper(...args) {
    // using ...args instead of arguments to store "real" array in wrapper.calls
    wrapper.calls.push(args);
    return func.apply(this, args);
  }

  wrapper.calls = [];

  return wrapper;
}


延时装饰者

重要程度: *****

创建一个装饰者 delay(f, ms),该装饰者将 f 的每次调用延时 ms 毫秒。

例如:

function f(x) {
  alert(x);
}

// create wrappers
let f1000 = delay(f, 1000);
let f1500 = delay(f, 1500);

f1000("test"); // 在 1000ms 后显示 "test"
f1500("test"); // 在 1500ms 后显示 "test"

换句话说,delay(f, ms) 返回的是延迟 ms 后的 f 的变体。

在上面的代码中,f 是单个参数的函数,但是你的解决方案应该传递所有参数和上下文 this

解决方案:

function delay(f, ms) {

  return function() {
    setTimeout(() => f.apply(this, arguments), ms);
  };

}

let f1000 = delay(alert, 1000);

f1000("test"); // shows "test" after 1000ms

注意这里是如何使用箭头函数的。我们知道,箭头函数没有自己的 thisarguments,所以 f.apply(this, arguments) 从包装器中获取 thisarguments

如果我们传递一个常规函数,setTimeout 将调用它且不带参数和 this=window(假设我们在浏览器环境)。

我们仍然可以通过使用中间变量来传递正确的 this,但这有点麻烦:

function delay(f, ms) {

  return function(...args) {
    let savedThis = this; // 将 this 存储到中间变量
    setTimeout(function() {
      f.apply(savedThis, args); // 在这儿使用它
    }, ms);
  };

}


去抖装饰者

重要程度: *****

debounce(f, ms) 装饰者的结果应该是一个包装器,该包装器最多允许每隔 ms 毫秒将调用传递给 f 一次。

换句话说,当我们调用 “debounced” 函数时,它保证之后所有在距离上一次调用的时间间隔少于 ms 毫秒的调用都会被忽略。

例如:

let f = debounce(alert, 1000);

f(1); // 立即执行
f(2); // 被忽略

setTimeout( () => f(3), 100); // 被忽略(只过去了 100 ms)
setTimeout( () => f(4), 1100); // 运行
setTimeout( () => f(5), 1500); // 被忽略(距上一次运行不超过 1000 ms)

在实际中,对于那些用于检索/更新某些内容的函数而言,当我们知道在短时间内不会有什么新内容的时候时,debounce 就显得很有用,因此最好不要浪费资源。

解决方案
function debounce(f, ms) {

  let isCooldown = false;

  return function() {
    if (isCooldown) return;

    f.apply(this, arguments);

    isCooldown = true;

    setTimeout(() => isCooldown = false, ms);
  };

}

debounce 的调用返回一个包装器。这儿可能会有两种状态:

  • isCooldown = false —— 准备好执行。
  • isCooldown = true —— 等待时间结束。

在第一次调用时,isCooldownfalse,因此调用继续进行,状态变为 true

isCooldowntrue 时,所有其他调用都被忽略。

然后 setTimeout 在给定的延时结束后,将其恢复为 false


节流装饰者

重要程度: *****

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

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

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

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

在浏览器中,我们可以设置一个函数,使其在每次鼠标移动时运行,并获取鼠标移动时的指针位置。在使用鼠标的过程中,此函数通常会执行地非常频繁,大概每秒 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(f, ms) {

  let isThrottled = false,
    savedArgs,
    savedThis;

  function wrapper() {

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

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

    isThrottled = true;

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

  return wrapper;
}

调用 throttle(f, ms) 返回 wrapper

  1. 在第一次调用期间,wrapper 只运行 f 并设置冷却状态(isThrottled = true)。
  2. 在这种状态下,所有调用都记忆在 savedArgs/savedThis 中。请注意,上下文和参数(arguments)同等重要,应该被记下来。我们同时需要他们以重现调用。
  3. ……然后经过 ms 毫秒后,触发 setTimeout。冷却状态被移除(isThrottled = false),如果我们忽略了调用,则将使用最后记忆的参数和上下文执行 wrapper

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

大家还有其他较常用的欢迎评论,笔者会同步更新~~