设计模式结构型之装饰器模式

实验介绍

本实验主要为大家介绍设计模式中的装饰器模式。从装饰器的概念引入,详细的介绍了装饰器和装饰器的应用,帮助大家对其有一个深层的理解。随后提供了两个在实际开发过程中可能会遇到的真实场景,帮助大家建立装饰器模式在前端应用的直观印象。最后提供了使用装饰器时候需要注意的点,能让大家正确的使用。

知识点

  • 装饰器模式介绍
  • 装饰器的应用
  • 使用装饰器的注意点
  • 实验挑战

装饰器模式介绍

装饰器模式(Decorator Pattern),它允许向一个现有的对象添加新的功能,而又不会改变其结构。

要谈装饰器“模式”,不如我们直接来讲讲什么是装饰器,在你了解装饰器之后,就能明白所谓装饰器模式实际上就是对装饰器的一种实现模式,或者说是一种实现思路。

装饰器

先来看一下例子:

假设这样一个场景,有一个函数 calculate(x) ,我们都知道,当参数 x 是一样的时候,返回的结果必然是一样的。

但是这个函数有一个缺点,就是每一次执行这个函数会耗费大量的时间,如果我们需要经常调用该函数,则必然会导致执行效率低下。

新建一个 index.html 文件及 calculate.js 文件,并引入。

就像这样:

// calculate.js

function calculate(x) {
  // ... 计算过程的代码

  console.log("十分耗费性能!!!");
  return x;
}

calculate(1); // 十分耗费性能!!!
calculate(2); // 十分耗费性能!!!

因此我们需要思考一种办法来避免重复的计算,使用缓存是一个不错的选择。

而关键在于,我们上面的 calculate(x) 函数内有大量复杂的逻辑,去在它的内部改变要付出很大的代价,因此要另寻方法去实现我们的目标。

装饰器可以帮助我们。

首先,由于我们不能改动原本的 calculate(x) ,那么就只能新声明一个函数替我们实现缓存的功能。

就像这样的一个函数:

// 计算缓存 - 装饰器
function cachingDecorator(fn) {
  // 我们用 map 结构来保存数据
  const cache = new Map();

  // ...
}

我们在 cachingDecorator 方法申请了一个 Map 用于模拟缓存功能,那么如何实现缓存,要分为三个步骤:

  • 判断当前在缓存中是否存在该值;
  • 存在,则直接从缓存中获取数据并返回;
  • 不存在,通过 calculate(x) 计算结果,然后返回结果并将结果存入缓存中

根据这三个步骤,我们可以实现一个装饰器:

// calculate.js

// 计算缓存 - 装饰器
function cachingDecorator(fn) {
  // 我们用 map 结构来保存数据
  const cache = new Map();

  // 返回一个函数
  return function (x) {
    // 从缓存中读取
    if (cache.has(x)) {
      // 为了大家能方便的看到效果增加一条输出语句
      console.log(`从缓存中读取:${cache.get(x)}`);
      return cache.get(x);
    }
    // 执行
    const result = fn.call(this, x);

    // 为了大家能方便的看到效果增加一条输出语句
    console.log(`新值:${result}`);

    // 把新的值存入缓存
    cache.set(x, result);

    return result;
  };
}

上面的代码我们可以做一个简单的分析:

  1. 返回一个函数,是为了获取一个执行函数;
  2. 通过 map 的一些 API 来帮助实现缓存,例如:has 方法用于判断当前是否已经存在该值;getset 方法分别用于获取值和添加新值,set 时是以 x 为键,计算结果为值;
  3. 装饰器传入的的是一个函数 fn ,也就是我们的 calculate(x) 函数,而返回了一个新的函数,在这个新函数里可以传入对应的 x 参数。
  4. 代码中执行部分使用了 fn.call(this, x) ,有些同学会好奇为什么不直接使用 fn(x) 的方式,这一点比较复杂,我们会在后面单独为大家介绍,这里你可以先认为二者是一样的。

为了展示,我们可以去掉 calculate(x) 中的打印语句:

function calculate(x) {
  // ... 计算过程的代码
  return x;
}

那么接下来,就让我们看看,这个装饰器的实际效果怎么样吧。

第一步,先通过装饰器来获取一个执行计算函数 excute(x),它本质上就是一个具有缓存能力的 calculate(x) 函数

// calculate.js

// 将 calculate 函数做为参数传入装饰器
const excute = cachingDecorator(calculate);

第二步,首先传入参数 1 :

// calculate.js

excute(1); // 新值:1

可以看到,通过传入参数 1 ,我们得到打印输出为新值,表示经过 cache.has(x) 判断后,知晓当前缓存中并未存储该结果,因此需要计算,然后再将其存入缓存中。

第三步,再传入两次 1 :

// calculate.js

excute(1); // 从缓存中读取:1
excute(1); // 从缓存中读取:1

当我们再次传入 1 时,从打印结果可以看到这两次执行结果都是从缓存中读取的,即经过 cache.has(x) 判断后,知晓当前缓存中已经存在该值,那么只需要读取缓存即可,就能避免性能的损耗。

第四步,传入参数 2 ,验证完整性:

// calculate.js

excute(2); // 新值:2
excute(2); // 从缓存中读取:2

在这一步中,我们传入了新的参数,可以看到在第一次时仍然是通过计算获取值,并存入缓存中,而第二次则可以直接从缓存中读取。

通过这样一个完整例子,相信大家对装饰器的作用已经有了一定的认识,现在来小结一下吧。

在装饰器 cachingDecorator 中,我们并未对 calculate 函数内部进行修改,保证了原函数的完整性和正确性,同时又能够避免重复的计算,进而提升性能。

这一点和装饰器模式的定义正好吻合,装饰器模式:向一个现有的对象添加新的功能,而又不会改变其结构。因此你会发现,装饰器模式的思想就是我们实现装饰器的思路与过程。

fn.call 的简单分析

上面我们提到为什么不适用 fn(x) 的方式去直接执行传入的函数,这里可以试一下,把 cachingDecorator(fn) 装饰器中的 fn 执行修改一下,就像这样:

function cachingDecorator(fn) {
  // ...

  // 返回一个函数
  return function (x) {
    // ...

    // 执行
    const result = fn(x);

    // ...

    return result;
  };
}

各位同学可以自行尝试一下,你会发现结果并不会受影响,因此自然会更加奇怪为何要多此一举的使用 func.call 这种形式。

实际上,之所以使用 func.call 是为了设定上下文,我们来看一个对象中的方法:

// calculate.js

const work = {
  calculate() {
    return 1;
  },

  slow(x) {
    // ... 这里省略了大量的计算任务
    return x * this.calculate(); // (*)
  },
};

当我们直接调用 work.slow 时,是没有问题的:

// calculate.js

console.log(work.slow(1)); // 1
console.log(work.slow(2)); // 2

然而,当我们通过装饰器对其增加缓存能力时,如果你采用 fn(x) 的方式,则会导致异常:

// calculate.js

work.slow = cachingDecorator(work.slow);
work.slow(1); // Cannot read properties of undefined (reading 'calculate') ...

注意:这里需要在 calculate.js 文件的第一行添加 "use strict",以保证是在严格模式下运行,此时 this 是等于 undefined 的,否则 this 将会指向 Window 对象,从而导致结果不会抛出异常,而是会打印出来 NaN

错误发生在试图访问 this.calculate() 时,这是因为当这样调用时,函数将得到 this = undefined ,而你无法从一个 undefined 中再去读取属性或方法。

当我们采用 func.call 的方式去设定好上下文的 this 后,则不会出现上述问题。

注意:关于 thiscall 涉及的知识点太多,对这块内容不清楚的同学可以参考下面两篇文章进行学习:

到这里关于装饰器的介绍就告一段落了,装饰器的理解实际上并不复杂,希望大家结合例子认真学习。

接下来,我们会介绍装饰器的一些应用场景,以帮助大家对装饰器和装饰器模式有一个更加具体的认知。

call 修改this中 content上下文,是如何修改

装饰器的应用

在前端中,有很多场景会用到装饰器模式,接下来我们会举几个例子。

防抖装饰器

请考虑这样一个场景,我们此时存在一个输入框,用户可以输入任意的内容,而只要用户输入完成时我们都需要向服务器发送一个请求。

如果用户每输入一个字符都发起一次请求,则会导致服务器压力陡增,因此没有必要为每一个字符的输入都发送请求。相反,我们想要等一段时间,然后处理整个结果。

这实际上就是前端中所谓的防抖功能,其定义是:在连续的多次触发同一事件的情况下,给定一个固定的时间间隔(假设 300 ms),该时间间隔内若存在新的触发,则清除之前的定时器并重新计时( 重新计时 300 ms )

具体表现为:在短时间多次触发同一事件,只会执行一次函数(最后触发的那次)。

我们可以给出一个具体的示例图以帮助大家理解:

图片描述

新建一个 debounce.js 文件,并引入。

给出一个防抖装饰器的实现函数:

// debounce.js

function debounce(func, ms) {
  // 记录计时器
  let timeout = null;

  // 通过闭包隐藏 debounce 函数内部变量,避免外部意外修改
  return function () {
    // 清除计时器
    clearTimeout(timeout);

    // 通过 setTimeout 实现延迟执行 func 函数
    timeout = setTimeout(() => func.apply(this, arguments), ms);
  };
}

为了帮助大家实际的了解防抖效果,可以先如下修改 index.html 文件:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    设计模式结构型之装饰器模式
    <div>
      <p>没有防抖处理:</p>
      <input id="input1" placeholder="请输入" />
      <p>结果展示:</p>
      <p id="p1"></p>
      <br />
      <p>加防抖处理:</p>
      <input id="input2" placeholder="请输入" />
      <p>结果展示:</p>
      <p id="p2"></p>
    </div>

    <script src="./debounce.js"></script>
  </body>
</html>

具体运行方式可以参考前言。

我们首先看一下不做防抖处理的情况,在 debounce.js 中添加代码:

// debounce.js

// 展示
function printf1(e) {
  document.getElementById("p1").innerText = e.target.value;
}

// 监听 id 为 input1 的输入框
document.getElementById("input1").addEventListener("input", function (e) {
  printf1(e);
});

运行后,可以看到,你的输入会马上展示在结果展示中:

图片描述

当我们使用防抖进行处理后,在 debounce.js 中添加代码:

// debounce.js

function printf2(e) {
  document.getElementById("p2").innerText = e.target.value;
}

document.getElementById("input2").addEventListener("input", (e) => {
  debounce(printf2, 1000)(e);
});

大家可以自行尝试一下,具体的效果是:当你在输入框中输入内容后,1s 后才会在结果展示中出现:

图片描述

到这里相信大家对防抖装饰器的定义和使用都有了一定的认识,当然了,除了输入操作,像一些鼠标移动之类的事件也都有可能用到该装饰器。

React 高级组件(HOC)

除了上面的防抖装饰器,还有很多装饰器的应用,例如本小节将会简单提到的 React 高级组件(HOC) 。

对于 React 大家应该至少都听过,而在 React 中如何实现复用组件逻辑呢,采用 HOC 在某些时候是一个不错的选择。

高阶组件是参数为组件,返回值为新组件的函数。 – 引自 react 官网 HOC 部分

const EnhancedComponent = higherOrderComponent(WrappedComponent);

高阶组件的使用和装饰器的使用思路基本一致的,我们将组件作为参数传入 higherOrderComponent 装饰器中,随后在不改变传入组件的情况下添加一些新的能力。

关于 React 的高阶组件具体内容我们不在这里做扩展,我们的初始目的是为了介绍装饰器。希望了解 HOC 的同学可以查阅:高阶组件 HOC

装饰器的应用

在前端中,有很多场景会用到装饰器模式,接下来我们会举几个例子。

防抖装饰器

请考虑这样一个场景,我们此时存在一个输入框,用户可以输入任意的内容,而只要用户输入完成时我们都需要向服务器发送一个请求。

如果用户每输入一个字符都发起一次请求,则会导致服务器压力陡增,因此没有必要为每一个字符的输入都发送请求。相反,我们想要等一段时间,然后处理整个结果。

这实际上就是前端中所谓的防抖功能,其定义是:在连续的多次触发同一事件的情况下,给定一个固定的时间间隔(假设 300 ms),该时间间隔内若存在新的触发,则清除之前的定时器并重新计时( 重新计时 300 ms )

具体表现为:在短时间多次触发同一事件,只会执行一次函数(最后触发的那次)。

我们可以给出一个具体的示例图以帮助大家理解:

图片描述

新建一个 debounce.js 文件,并引入。

给出一个防抖装饰器的实现函数:

// debounce.js

function debounce(func, ms) {
  // 记录计时器
  let timeout = null;

  // 通过闭包隐藏 debounce 函数内部变量,避免外部意外修改
  return function () {
    // 清除计时器
    clearTimeout(timeout);

    // 通过 setTimeout 实现延迟执行 func 函数
    timeout = setTimeout(() => func.apply(this, arguments), ms);
  };
}

为了帮助大家实际的了解防抖效果,可以先如下修改 index.html 文件:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    设计模式结构型之装饰器模式
    <div>
      <p>没有防抖处理:</p>
      <input id="input1" placeholder="请输入" />
      <p>结果展示:</p>
      <p id="p1"></p>
      <br />
      <p>加防抖处理:</p>
      <input id="input2" placeholder="请输入" />
      <p>结果展示:</p>
      <p id="p2"></p>
    </div>

    <script src="./debounce.js"></script>
  </body>
</html>

具体运行方式可以参考前言。

我们首先看一下不做防抖处理的情况,在 debounce.js 中添加代码:

// debounce.js

// 展示
function printf1(e) {
  document.getElementById("p1").innerText = e.target.value;
}

// 监听 id 为 input1 的输入框
document.getElementById("input1").addEventListener("input", function (e) {
  printf1(e);
});

运行后,可以看到,你的输入会马上展示在结果展示中:

图片描述

当我们使用防抖进行处理后,在 debounce.js 中添加代码:

// debounce.js

function printf2(e) {
  document.getElementById("p2").innerText = e.target.value;
}

document.getElementById("input2").addEventListener("input", (e) => {
  debounce(printf2, 1000)(e);
});

大家可以自行尝试一下,具体的效果是:当你在输入框中输入内容后,1s 后才会在结果展示中出现:

图片描述

到这里相信大家对防抖装饰器的定义和使用都有了一定的认识,当然了,除了输入操作,像一些鼠标移动之类的事件也都有可能用到该装饰器。

React 高级组件(HOC)

除了上面的防抖装饰器,还有很多装饰器的应用,例如本小节将会简单提到的 React 高级组件(HOC) 。

对于 React 大家应该至少都听过,而在 React 中如何实现复用组件逻辑呢,采用 HOC 在某些时候是一个不错的选择。

高阶组件是参数为组件,返回值为新组件的函数。 – 引自 react 官网 HOC 部分

const EnhancedComponent = higherOrderComponent(WrappedComponent);

高阶组件的使用和装饰器的使用思路基本一致的,我们将组件作为参数传入 higherOrderComponent 装饰器中,随后在不改变传入组件的情况下添加一些新的能力。

关于 React 的高阶组件具体内容我们不在这里做扩展,我们的初始目的是为了介绍装饰器。希望了解 HOC 的同学可以查阅:高阶组件 HOC

使用装饰器的注意点

装饰器虽然可以帮助我们实现在不改动原函数的基础上添加一些新的功能。但是仍然有一些需要我们在使用时注意的地方。

第一个需要注意的点是:使用装饰器的情况下,需要注意原函数是否存在函数属性

新建一个 point.js 文件,并引入。

我们可以来试验一下,首先为 calculate 函数添加函数属性 description 用于对函数作用进行描述,就像这样:

// point.js

function calculate(x) {
  return x;
}

// 在 calculate 上添加一个描述属性
calculate.description = "这是一个十分耗费性能的计算函数!";

// 打印输出:calculate.description: 这是一个十分耗费性能的计算函数!
console.log("calculate.description:", calculate.description);

通过打印输出,你可以看到我们是能够正常的读取 calculate 函数的属性的。

那么当我们去构建一个装饰器,并将 calculate 传入后,获取的新的计算器还能正常获取到属性吗?就像这样:

// point.js

const excute = cachingDecorator(calculate);
console.log("excute.description:", excute.description); // undefined

聪明的你一定能发现,这里的 excute 已经是一个新函数,那么原函数的函数属性是没有的。

因此,如果你使用装饰器,那么需要注意原函数是否存在属性。不过这不是无法解决的,存在一种创建装饰器的方法,该装饰器可保留对函数属性的访问权限,但这需要使用特殊的 Proxy 对象来包装函数。这里我们不再扩展这一部分,有兴趣的同学可以主动去了解一下。

第二个需要注意的点是:在 cachingDecorator 这个装饰器中,执行 fn 时采用了 fn.call 的方式,这一点的原因在前面的内容中已经讲过了,这里就不再赘述了。

以上两点是需要大家在使用装饰器的时候需要额外注意的。

实验挑战

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

在上面我们举例时,提到了防抖装饰器,那么本实验的挑战则是实现一个节流装饰器。

节流:在固定时间间隔(一个时间周期)内,大量触发同一事件,触发事件只会执行一次。周期结束,再次触发事件则开始新的周期。

为了方便大家理解,下面配上一张示意图:

图片描述

何时应该使用节流,例如:我们想要跟踪鼠标移动。

在浏览器中,我们可以设置一个函数,使其在每次鼠标移动时运行,并获取鼠标移动时的指针位置。在使用鼠标的过程中,此函数通常会执行地非常频繁,大概每秒 100 次(每 10 毫秒)。此时我们需要限制这个执行次数,我们只希望在一段时间内该函数只执行一次。此时就可以想到节流装饰器。

在了解完节流的概念后,希望大家实现一个节流装饰器。

答案代码放在实验最后课程的源码包里,大家可以自行下载。

实验总结

实验为大家介绍了装饰器模式,这个模式本质上就是对装饰器的实现思路,或者说实现过程。因此你需要深入的学习装饰器的各种知识。文章不仅讲解了基本的概念,还为大家带来了两个前端中具体的应用实例,帮助大家建立直观的印象。希望通过本实验的学习,能让同学们对这一模式有一个较为清晰的认识。

本节实验源码压缩包下载链接:装饰器模式源码

posted @   雨晨*  阅读(14)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
点击右上角即可分享
微信分享提示