[封装03-设计模式] Decorator 装饰器模式在前端的应用

666 阅读8分钟

导航

[react] Hooks

[封装01-设计模式] 设计原则 和 工厂模式(简单抽象方法) 适配器模式 装饰器模式
[封装02-设计模式] 命令模式 享元模式 组合模式 代理模式
[封装03-设计模式] Decorator 装饰器模式在前端的应用

[React 从零实践01-后台] 代码分割
[React 从零实践02-后台] 权限控制
[React 从零实践03-后台] 自定义hooks
[React 从零实践04-后台] docker-compose 部署react+egg+nginx+mysql
[React 从零实践05-后台] Gitlab-CI使用Docker自动化部署

[源码-webpack01-前置知识] AST抽象语法树
[源码-webpack02-前置知识] Tapable
[源码-webpack03] 手写webpack - compiler简单编译流程
[源码] Redux React-Redux01
[源码] axios
[源码] koa
[源码] vuex
[源码-vue01] data响应式 和 初始化渲染
[源码-vue02] computed 响应式 - 初始化,访问,更新过程
[源码-vue03] watch 侦听属性 - 初始化和更新
[源码-vue04] Vue.set 和 vm.$set
[源码-vue05] Vue.extend

[源码-vue06] Vue.nextTick 和 vm.$nextTick

[源码-react01] ReactDOM.render01
[源码-react02] 手写hook调度-useState实现

[部署01] Nginx
[部署02] Docker 部署vue项目
[部署03] gitlab-CI

[数据结构和算法01] 二分查找和排序

[深入01] 执行上下文
[深入02] 原型链
[深入03] 继承
[深入04] 事件循环
[深入05] 柯里化 偏函数 函数记忆
[深入06] 隐式转换 和 运算符
[深入07] 浏览器缓存机制(http缓存机制)
[深入08] 前端安全
[深入09] 深浅拷贝
[深入10] Debounce Throttle
[深入11] 前端路由
[深入12] 前端模块化
[深入13] 观察者模式 发布订阅模式 双向数据绑定
[深入14] canvas
[深入15] webSocket
[深入16] webpack
[深入17] http 和 https
[深入18] CSS-interview
[深入19] 手写Promise
[深入20] 手写函数
[深入21] 数据结构和算法 - 二分查找和排序
[深入22] js和v8垃圾回收机制
[深入23] JS设计模式 - 代理,策略,单例
[深入24] Fiber
[深入25] Typescript
[深入26] Drag

[前端学java01-SpringBoot实战] 环境配置和HelloWorld服务
[前端学java02-SpringBoot实战] mybatis + mysql 实现歌曲增删改查
[前端学java03-SpringBoot实战] lombok,日志,部署
[前端学java04-SpringBoot实战] 静态资源 + 拦截器 + 前后端文件上传
[前端学java05-SpringBoot实战] 常用注解 + redis实现统计功能
[前端学java06-SpringBoot实战] 注入 + Swagger2 3.0 + 单元测试JUnit5
[前端学java07-SpringBoot实战] IOC扫描器 + 事务 + Jackson
[前端学java08-SpringBoot实战总结1-7] 阶段性总结
[前端学java09-SpringBoot实战] 多模块配置 + Mybatis-plus + 单多模块打包部署
[前端学java10-SpringBoot实战] bean赋值转换 + 参数校验 + 全局异常处理
[前端学java11-SpringSecurity] 配置 + 内存 + 数据库 = 三种方式实现RBAC
[前端学java12-SpringSecurity] JWT
[前端学java13-SpringCloud] Eureka + RestTemplate + Zuul + Ribbon

复习笔记-01
复习笔记-02
复习笔记-03
复习笔记-04

前置知识

(1) 一些单词

decorator 装饰器
custom 定制 习惯
edition 版本
reversion 反转 逆转

(2) 自定义事件 - CustomEvent

  • 概念:CustomEvent 用来生成自定义的事件实例
  • 特点
    • 要在触发事件的同时,传入指定的 ( 数据 ),就可以使用 CustomEvent 接口生成自定义事件对象
    • 浏览器预定义事件的缺点
      • 那些浏览器预定义的事件,虽然可以手动生成,但是往往不能在事件上绑定数据
  • 对比
    • new Event(type, options)
    • new CustomEvent(type, options)
    • EventTarget.dispatch(event事件实例)
前置知识
---

1
EventTarget.dispatchEvent(event)
- 作用
  - 在 ( 当前节点 - EventTarget ) 上触发 ( 指定的事件 - event事件实例 ),从而 ( 触发监听函数 ) 的执行
  - event事件实例如何生成 - 通过 new Event() 生成
- 参数
  - 是一个 Event 对象的实例
- 返回值
  - 返回一个boolean值
  - Event.preventDefault() 返回false,否则返回true
  

2
Event 对象 | 构造函数
- 概念
  - 浏览器提供了原生的Event对象
  - (1) 所有的 ( 事件 ) 都是 ( Event ) 对象的 ( 实例 )
  - (2) ( 事件实例对象 ) 继承了 ( Event.prototype对象 )
  - (3) 事件实例 可以通过 new Event() 来生成
- 语法
  - const event = new Event(type, options)
  - 参数
    - type是一个字符串,表示事件名称
    - options是一个对象,表示事件配置对象
      - bubble
        - 表示事件是否可以冒泡,( boolean值,可选,默认是false )
      - cancelable
        - 表示事件是否可以通过 Event.preventDefault() 被取消;( boolean值,可选,默认是false )
        - 一旦事件被取消,就好像从来没有发生过,不会触发浏览器对该事件的默认行为

3
案例
<!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 id="one">通过EventTarget.dispatch(event)来触发事件</div>
    <button id="two">触发click事件的button</button>
    <script>
      // 效果
      // - 1. 点击 button 触发 click 事件,执行click事件绑定的监听函数
      // - 2. 在上面 1 的监听函数中,执行 EventTarget.dispatch(event) 触发 div上绑定的hello1事件,从而触发 hello1 事件的监听函数 打印hello
      const one = document.getElementById("one");
      const hello2 = new Event("hello1", { 
        // ---------------------------------------- 生成 hello2 事件,hello2事件的名称是 'hello1'
        bubbles: true, // 是否冒泡
        cancelable: true, // 是否可以通过 Event.preventDefault() 来取消
      });
      one.addEventListener("hello1", (e) => { 
        // ---------------------------------------- 监听事件名称是 'hello1' 的事件,事件触发时,会执行监听函数
        console.log("hello");
      });

      const two = document.getElementById("two");
      two.addEventListener("click", () => {
        console.log("click事件回调触发");
        one.dispatchEvent(hello2); 
        // 触发 one 节点上的 hello2 事件
        // EventTarget.dispatchEvent(event事件实例),在 one 节点上触发 hello2 事件,hello2事件通过 new Event() 生成,指定的事件名事hello1,hello1事件被addEventListener监听
      });
    </script>
  </body>
</html>
1
new CustomEvent(type, options)
- 参数
  - type 表示事件名称一个字符串,必传参数
  - options 事件的配置对象 可选
    - detail:该属性表示 ( 事件附带的数据 ),默认值是null
- 作用
  - Custom- CustomEvent 构造函数用来生成 customEvent 事件实例Event 构造函数用来生成 customEvent 事件实例、
  
- 案例
const button3 = document.getElementById("three");
const customEvent = new CustomEvent("hello3", {
  bubbles: false,
  cancelable: false,
  detail: {
    effect: "customEvent可以携带数据", // new CustomEvent() 相对于 new Event() 在第二个参数配置对象中,多了 detail 属性,表示 ( 事件附带的数据 )
  },
});
button3.addEventListener("click", () => {
  button3.dispatchEvent(customEvent);
});
button3.addEventListener("hello3", (e) => {
  console.log(`e.detail`, e.detail);
});

(3) HOC 高阶组件

  • HOC本质上不是组件,而是一个 ( 函数 ),该函数接收一个组件作为参数,返回一个新的组件
  • 实现高阶组件的方式
    • 属性代理 props proxy -----------> 代理
    • 反向继承 inhert inversion -----> 继承
  • ( 属性代理 ) 实现高阶组件
    • 本质:使用组合的方式,通过容器组件对组件的包装,在容器组件中实现功能
    • 特点:因为是组件对组件的嵌套,即存在父子组件生命周期执行顺序问题和state,props状态改变带来的影响
    • 多种属性代理的实现方式:
      • 操作props:在外层容器组件中拦截,处理props,然后传入被包装的组件
      • 抽象state:在容器组件中,定义state和操作state的方法,然后传给子组件
      • ref的引用:ref只能用于类组件,不能用于函数组件,但是hooks提供了useRef,forwardRef,useImperativeHandle属性
  • ( 反向继承 ) 实现高阶组件
    • 反向继承 - 实现HOC高阶组件的原理
      • 1.通过 extends 的方式继承被包装的组件
      • 2.然后在高阶组件的render函数中,通过 super 对象获取到被装组件原型上的 render 方法
      • 3.通过以上12,能获取到被包装组件的原型上的所有属性和方法,比如render,生命周期,state等,那么就能实现不同的功能:比如条件渲染,获取|重写生命周期,修改state等
    • super
      • super可以作为函数,也可以作为对象
        • super作为函数时
          • 表示的是父类的构造函数,只能用于构造函数中,this指向子类实例
        • super作为对象时
          • 在普通方法中:表示父类的原型,this指向子类实例
          • 在静态方法中:表示父类,this指向当前子类
      • 注意
        • es6的class是存在两条原型链的
        • 1.A 的原型对象是 B
        • 2.A.prototype 的原型对象 B.prototype
  • 属性代理 和 反向继承 的对比
    • 属性代理:是通过 ( 组合-装饰器模式 ) 的方式,从 ( 外部 ) 去操作 props 来实现不同的功能
    • 反向继承:是通过 ( 继承 ) 的方式,从 ( 内部 ) 去获取组件内部的属性和方法,比如render,state,生命周期等

(4) 处理URL

1
const url = new URL(url [, base])
- 参数
  - url
    - 一个表示 ( 绝对或者相对 ) URL 的 DOMString
    - 绝对URL:base参数将被忽略
    - 相对URL:会把 base 参数作为基准URL
  - base 
    - 一个基准URL的DOMString
    - base参数生效的前提是:第一个参数url是相对URL
- 返回值
  - 新生成的url实例对象
  - 如果给定的基本 URL 或生成的 URL 不是有效的 URL 链接,则会抛出一个`TypeError`
  - !!!!!! --->  url.searchParams() 和 new URLSearchParams() 的返回值一样
  - !!!!!! --->  new URL('https://.../?s=url').searchParams.get('s') === new URLSearchParams('?s=url').get('s')
- 例子
  const url = new URL('/api/?author=woow_wu7&age=20#head', 'http://www.baidu.com')
  // 等同于 const url = new URL('http://www.baidu.com/api/?autho=woow_wu7&age=20#head')
  // href: "http://www.baidu.com/api/?author=woow_wu7&age=20#head"
  // 注意:这里 query 一定要在 hash 前面,不然取不到query,会把query部分也作为hash
  
  
2
const URLSearchParams = new URLSearchParams(init)
- 参数
  - init:一个url信息的string
- 返回
  - URLSearchParams 实例
- 实例对象上的属性
  - append(key, value) 添加search键值对
  - delete(key) 删除
  - entries() 返回迭代器对象 - 可以被 for...of 遍历
  - forEach() 遍历
  - get(key) 获取
  - getAll(key) 因为可以存在相同key的情况,对应不同的value值
  - has(key) 是否存在
  - keys()
  - set(key, value)
  - sort()
  - toString()

image.png

(5) vue -> vm.listenersvm.listeners 和 vm.attrs 和 vm.$slots

  • vm.$listeners
    • 包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器
    • 可以通过 v-on="$listeners" 传入内部组件——在创建更高层次的组件时非常有用
    • this.$listeners.click 获取 v-on:click="go" 指定的go方法
  • vm.$attrs
    • 包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定,除了 ( class 和 style )
    • 可以通过 v-bind="$attrs" 传入内部组件——在创建高级别的组件时非常有用
  • vm.$slots
    • 用来访问被 ( 插槽分发 ) 的内容
    • v-slot:foo 中的内容将会在 vm.$slots.foo 中被找到
    • 默认插槽中的内容可以通过 vm.$slots.default 中被找到
    • 请注意插槽不是响应性的
  • 例子

(一) 装饰器模式定义

  • 类型:装饰器模式是 结构型设计模式,即不改变原有结构
  • 别名
    • wrapper
    • 表示在原有的基础上再包装上一层
  • 特点
    • 不修改原来的 对象|类|函数 ,所以满足 - 开放封闭原则,对扩展开放,对修改封闭
    • 不使用 继承
    • 动态的添加新功能
  • 常用的装饰器类型
    • 常用的装饰器有 ( 类装饰器 ) ( 属性装饰器 ) ( 方法装饰器 ) ( 参数装饰器 )
  • 举例1
    • 圣诞节我们做了一个圣诞树,然后又加了彩灯,添加彩灯就是装饰器模式
  • 举例2
    • 需求:一张照片,现在的需求:需要使照片更结实,并能挂在墙上去
    • 实现:我们添加一个相框,那么现在除了照片本身,多了相框之后就具有挂在墙上和保护等新的功能,我们并没有去改变照片本身,而是在外面加了相框
    • 扩展:以后我们还要晚上也能看到,还可以继续进行包装,加上灯等

(二) 装饰器模式在前端中的应用 - 数据上报

1
需求:
1. 点击按钮登录
2. 现在我们点击按钮,除了能登录,还要做一些其他的事情,比如上报一些数据


2
未使用Decorator装饰器模式前的实现:
缺点:我们直接在原来的函数中添加了逻辑,违反了 ( 单一职责原则 和 开放封闭原则 )
单一职责原则:一个函数只包含一个任务逻辑
开放封闭原则:可扩展,不可修改;对扩展开放对修改封闭
function login() { // 违反了单一职责原则和开放封闭原则
  console.log('登录')
  console.log('数据上报')
}



3 
函数版本 - 使用Decorator实现
<!DOCTYPE html>
<html lang="en">
  <body>
    <button id="button">按钮</button>
    <script>
      // 装饰器模式
      // 1. 没有直接修改login的逻辑
      // 2. 而是通过 after 函数 使用装饰器模式 对函数进行包装,实现了功能的扩展
      const login = (params) => {
        console.log("login");
      };
      const report = (params) => {
        console.log("report");
      };
      const after = (...fns) => {
        return (...rest) => {
          // 这里返回了一个函数,所以你还可以对after继续使用Decorator装饰器模式进行函数功能的扩展
          fns.forEach((fn) => fn(...rest));
        };
      };
      const loginDecorator = after(login, report); // 类比一下redux中的compose函数,看看是否需要 执行结果迭代 和 每个函数参数的迭代
      const handleClick = () => {
        loginDecorator("data");
      };

      const button = document.getElementById("button");
      button.addEventListener("click", handleClick, false);
    </script>
  </body>
</html>



4
class版本 - 使用Decorator实现
<!DOCTYPE html>
<html lang="en">
  <body>
    <button id="button">点击</button>
    <script>
      class Login {
        login() {
          console.log("login");
        }
      }
      class Report {
        report() {
          console.log("report");
        }
      }
      class After {
        constructor(Login, Report) {
          this.login = new Login();
          this.report = new Report();
        }
        after() {
          this.login.login();
          this.report.report();
        }
      }
      const handleClick = () => {
        const after = new After(Login, Report);
        after.after();
      };
      const button = document.getElementById("button");
      button.addEventListener("click", handleClick, false);
    </script>
  </body>
</html>

(三) 装饰器模式在前端中的应用 - 表单校验

  • 在提交表单前做校验工作
/**
 * 借助装饰器模式,很容易衍生出 AOP 面向切面编程的概念
 * - 场景:典型场景就是对表单的验证,我们将把表单校验逻辑 validate 函数融入到 before 逻辑当中
 * - 具体:
 *   1. 在提交表单时,执行 ( submit.before ) 函数,因为在 ( Function.prototype.before ) 挂载了 ( before ) 函数,被所有 ( 函数实例 ) 所继承
 *   2. 执行 ( submit.before ) 从而在 submit 之前执行验证函数 ( validate ) 从而在执行submit之前做表单验证功能
 */

<!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>
    <button id="button">submit</button>
    <script>
      function submit(params) { // ---- 提交
        console.log("submit ->", params);
      }
      function validate(params) { // -- 校验
        console.log("validate ->", params);
        return true;
      }
      Function.prototype.before = function (validateFn) { // ---- before
        const self = this; // this -> submit
        return function () {
          const isValidate = validateFn.apply(this, arguments);
          return isValidate ? self.apply(this, arguments) : "未通过校验";
        };
      };

      const button = document.getElementById("button");
      const handle = () => {
        submit.before(validate)("校验的数据和提交的数据都在这里"); // 这里也可以像案例1中那样单独写 before 函数
      };
      button.addEventListener("click", handle, false);
    </script>
  </body>
</html>

(四) 装饰器模式在前端中的应用 - HOC

  • 需求
    • 在vue中所有页面的按钮,都添加debounce防抖功能
<!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>
    <script src="./dist/vue.js"></script>
  </head>
  <body>
    <!-- 在vue中使用HOC实现给每个button添加debounce功能 -->
    <div id="root">
      <hoc-button @click="clickFather">HocButton</hoc-button>
    </div>
    <script>
      Vue.component("HocButton", {
        props: {},
        // template: `
        //   <button @click="clickChild">
        //     HocButton
        //   </button>
        // `,
        // 使用上面的 template 也可以,这里使用 render 函数来渲染,因为 template和render最终都会转成render函数
        render(createElement) {
          return createElement(
            "Button",
            {
              on: {
                click: this.clickChild,
              },
              props: this.$props,
              attrs: this.$attrs, // 包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外),配置上面props,能获取到所有组件上绑定的属性
              scopedSlots: this.$scopedSlots, // 透传 scopedSlots
            },
            this.$slots.default // 默认default
          );
        },
        methods: {
          debounce(fn, delay) {
            clearTimeout(fn.timer);
            fn.timer = setTimeout(fn, delay);
          },
          clickChild() {
            console.log("clickChild");
            this.debounce(this.$listeners.click, 1000);
            // 获取父作用域中的 click 事件的监听函数 clickFather
            // vm.$listeners
            // - 包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器
          },
        },
      });

      new Vue({
        el: "#root",
        data() {
          return {};
        },
        methods: {
          clickFather() {
            console.log("clickFather");
          },
        },
      });
    </script>
  </body>
</html>

image.png

资料