首页 > 其他分享 >设计模式结构型之装饰器模式

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

时间:2024-02-23 18:15:13浏览次数:23  
标签:缓存 函数 debounce 结构型 js 设计模式 装饰 calculate

实验介绍

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

知识点

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

装饰器模式介绍

装饰器模式(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 毫秒)。此时我们需要限制这个执行次数,我们只希望在一段时间内该函数只执行一次。此时就可以想到节流装饰器。

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

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

实验总结

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

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

标签:缓存,函数,debounce,结构型,js,设计模式,装饰,calculate
From: https://www.cnblogs.com/xzemt/p/18030132

相关文章

  • 设计模式结构型之适配器模式
    实验介绍本节实验为大家带来了适配器模式,适配器模式是作为两个不兼容的接口之间的桥梁,可以将变化都封装于它本身,提供简单统一的接口使用。从一个有趣的例子开始为大家逐步的讲解适配器,帮助大家学习其基本的概念。随后为大家介绍了适配器在前端中的真实应用,加深对适配器的认识。最......
  • 设计模式创建型之原型模式
    实验介绍本实验主要为大家介绍了前端中原型模式,为了加深大家对原型的了解,实验中花费大量篇幅讲解了原型及原型的概念,并配上了相关的例子以帮助大家学习。随后我们对class进行了简单的介绍,它可以被简单的认为是语法糖。最后,为了帮助大家理解原型中的克隆,实验也对浅拷贝与深拷贝......
  • 设计模式创建型之工厂模式
    基本概念在给出工厂模式的定义之前,不妨先来了解一下工厂的概念。通过百度百科查到的所谓工厂的定义:是一类用以生产货物的大型工业建筑物,即我们为工厂输送原料,经过工厂对原料进行处理加工之后会输出产物。例如下面这样一个例子:张三是一名大学生,毕业后为了上班方便就考虑买一台......
  • 设计模式创建型之单例模式
    实验介绍本实验主要介绍了设计模式中的单例模式,在前端领域中,有很多地方都运用到了单例模式的思维,例如目前的主流前端框架中所用到的Redux和Vuex。实验首先通过一个小例子为大家展示了单例模式的实现原理,随后通过完成一个自定义的Storage存储器来帮助大家加深对单例模式的理......
  • 3分钟看懂设计模式02:观察者模式
    一、什么是观察者模式观察者模式又叫做发布-订阅模式或者源-监视器模式。结合它的各种别名大概就可以明白这种模式是做什么的。其实就是观察与被观察,一个对象(被观察者)的状态改变会被通知到观察者,并根据通知产生各自的不同的行为。以下为《设计模式的艺术》中给出的定义:观察者......
  • [设计模式]创建型模式-抽象工厂模式
    简介抽象工厂模式是一种创建型设计模式,它提供了一种创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。抽象工厂模式将一组具有共同主题的单个工厂封装起来,它提供接口用于创建相关或依赖对象的家族,而不需要指定具体的类。抽象工厂模式包含以下几个核心角色:抽象工厂(A......
  • 3分钟看懂设计模式01:策略模式
    一、什么是策略模式定义一些列算法类,将每一个算法封装起来,并让它们可以互相替换。策略模式让算法独立于使用它的客户而变化,是一种对象行为型模式。以上是策略模式的一般定义,属于是课本内容。在没有真正理解策略模式之前并不需要对此定义下过多功夫,读一遍直接进入下一章节。二......
  • 设计模式浅析(六) ·命令模式
    设计模式浅析(六)·命令模式日常叨逼叨java设计模式浅析,如果觉得对你有帮助,记得一键三连,谢谢各位观众老爷......
  • 我们在SqlSugar开发框架中,用到的一些设计模式
    我们在《SqlSugar开发框架》中,有时候都会根据一些需要引入一些设计模式,主要的目的是为了解决问题提供便利和代码重用等目的。而不是为用而用,我们的目的是解决问题,并在一定的场景下以水到渠成的方式处理。不过引入任何的设计模式,都会增加一定的学习难度,除非是自己本身领会比较好了,......
  • [设计模式]创建型模式-简单工厂模式
    简介简单工厂模式又称为静态工厂模式,属于创建型模式,但不属于GOF23设计模式。由一个工厂对象决定创建出哪一种产品类的实例。简单工厂模式的实质是由一个工厂类根据传入的参数,动态决定应该创建哪一个产品类。简单工厂适用场景:工厂类负责创建的对象比较少;客户端只需要知道传入工厂......