装饰器模式

什么是装饰器模式?

装饰——对原有的物品进行外部修饰。就比如说,我们为小明购买礼物,然后用彩色纸和彩带将该物品包装起来,送给小明。这样就会显得用心和隆重。

装饰器模式:对原有代码不进行修改,而是直接在该代码外层包装一些必须添加的逻辑。定义:在不改变原对象的基础上,通过对其进行包装拓展,使原有对象可以满足用户的复杂需求。

通常我们使用装饰器模式都是在 类上使用通过 @xxxDecorate 对类或方法等进行装饰。

如何实现装饰器模式?

装饰器的本质上是一个函数,内部为装饰对象添加新的功能。通常情况下使用 @xxxDecorate | @xxxDecorate() 前者是装饰器,后者是装饰器工厂。

javascript 暂时不支持装饰器(提案已经进入 staged 3),我们需要通过一些工具实现。

babel 进行转换

当我们实现如下代码

function ClassDecorate(target) {
    target.printDecorateText = () => {
        console.log("我是装饰器: ", ClassDecorate.name);
    }
}

@ClassDecorate
class User {
    
}

User.printDecorateText();

@ClassDecorate 本质上是函数调用的语法糖,相当于 ClassDecorate(User),而 js引擎 现在还不支持该语法。 所以,我们需要通过 babel 将该代码装换成能够执行的代码。

安装babel及配置

  • 安装babel 、babel预设、babel对装饰器转换的插件、babel-cli

    • npm install babel-preset-env babel-plugin-transform-decorators-legacy --save-dev
      
      npm install -g babel-cli
      
  • 配置 .babelrc

    • {
          "presets": ["env"],
          "plugins": ["transform-decorators-legacy"]
      }
      

安装并配置后,我们就执行babel xxx.js --out-file tran-xxx.js 命令,该命令会在当前文件夹生成转换后的代码。

上面代码通过babel装换后的结果

  • "use strict";
    
    var _class;
    
    function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
    
    function ClassDecorate(target) {
        target.printDecorateText = function () {
            console.log("我是装饰器: ", ClassDecorate.name);
        };
    }
    
    var User = ClassDecorate(_class = function User() {
        _classCallCheck(this, User);
    }) || _class;
    
    User.printDecorateText();
    
    
  • 可以看到本质上其就是转换成了函数调用。

使用 typescript(推荐)

typescript 中默认是支持装饰器语法糖的,不过我们需要在 tsconfig.json 中开启 "experimentalDecorators": true // Enable experimental support for TC39 stage 2 draft decorators. ,然后我们就能够愉快的使用装饰器语法糖了。

装饰器分类

装饰器可以分为五类:类装饰器、方法装饰器、访问器装饰器、属性装饰器、参数装饰器。

  • 类装饰器

    • 它的参数只有一个,就是当前装饰的类。我们可以对类上的静态属性进行装饰。比如,为一个类的静态方法执行时添加执行打印信息。

    • type ClassStruct<T = any> = new (...args: unknown[]) => T;  
      function LogMethodInfo(target: ClassStruct) {
          // 为 printInfo 静态方法添加打印功能
          const originPrintInfo = <Function>Reflect.get(target, "printInfo");
      
          // @ts-expect-error
          target.printInfo = function (...args: unknown[]) {
              const res = originPrintInfo.apply(originPrintInfo, args);
              console.log(`${Date.now()}-${target.name}.${originPrintInfo.name}`);
              return res;
          }; 
          return 
      }
      
      @LogMethodInfo
      class User {
          static printInfo() {
              console.log("printInfo")
          }
      }
      
      User.printInfo(); /**   printInfo
                              1675090152544-User.printInfo */
      
    • 如此,我们对类的静态方法装饰就生效了。

  • 方法装饰器

    • 我们知道在js中实例的方法都是挂在到类的 prototype 属性上的。所以,该函数的有两个参数(target: 类.prototype, properkey: string | symbol, descriptor: 属性描述符) => any

    • // 方法装饰器
      function methodDecorator(name: string): MethodDecorator {
          return (target, propertyKey, descriptor: TypedPropertyDescriptor<any>) => {
              // 1. 获取原始方法
              const originMethod = <Function>descriptor.value!;
              // 2. 装饰该方法
              descriptor.value = function(...args: unknown[]) {
                  const res = originMethod.apply(this, args);
                  return `${res}-${name}`;
              };
          }
      }
      
      class User {
          @methodDecorator("ACWINK")
          getUserinfo() {
              return "acwink";
          }
      }
      
      
      const user = new User();
      console.log(user.getUserinfo()); // acwink-ACWINK
                              
      
  • 访问器装饰器

    • 他是用来装饰 get set 这样的访问器方法。他有三个参数 (target: prototype, propertyKey: string | symbol, descriptor: 访问修饰符) => any

    • function InjectHeader(header: string): MethodDecorator {
          return (target, properkey, descriptor: any) => {
              const originalSetter = descriptor.set;
              descriptor.set = function (newValue: string) {
                  const composed = `${header}-${newValue}`;
                  originalSetter.call(this, composed);
              }
          }
      }
      
      class User {
          _value!: string;
          
          get value() {
              return this._value;
          }
      
          @InjectHeader("ACWINK")
          set value(newValue: string) {
              this._value = newValue;
          }
      }
      
      
      const user = new User();
      user.value = "acwink";
      console.log(user.value); // ACWINK-acwink
      
  • 属性装饰器

    • 其实本质上我们并不能在对象实例化时对属性进行装饰,这里的属性装饰器其实还是在函数的prototype 上进行操作。(target: prototype, properkey: string | symbol) => unknow,单独使用作用不大。

    • function ModifyName(): PropertyDecorator {
          return (target: any, propperkey) => {
              target[propperkey] = "acwink";
              target["orther"] = "test";
          }
      }
      class User {
          @ModifyName()
          nickname!: string; 
          
      }
      
      const user = new User();
      // @ts-expect-error
      console.log(user.nickname, Reflect.getPrototypeOf(user)?.nickname); // acwink acwink
      
  • 参数装饰器

    • 单独使用作用不大。(target: prototype, properkey: stirng, index: 修饰参数在函数参数列表中的索引) => any

    • function CheckParam(): ParameterDecorator {
          return (target, methodIdentifier, index) => {
      const user = new User();
              console.log(target, methodIdentifier, index); // {} setName 0
          }
      }
      class User {
          setName(@CheckParam() name: string) {
          }
      }
      

装饰器模式在开源项目中的应用?

  • redux: connect

  • react: hoc(高阶组件)

    • import React from "react";
      import { useLocation, useNavigate, useParams } from "react-router";
      import { useSearchParams } from "react-router-dom";
      
      function withRouter<T>(WrapperComponent: React.FC<T>) {
        return function (props: T) {
          // 1. 导航
          const navigate = useNavigate();
          // 2. 动态路由的参数
          const params = useParams();
          // 3. 查询字符串的参数
          const location = useLocation();
          const [searchParams] = useSearchParams();
      
          const query = Object.fromEntries(searchParams);
      
          const router = { navigate, params, location, query };
      
          return <WrapperComponent {...props} router={router}></WrapperComponent>;
        };
      }
      
      export default withRouter;
      
    • 这个函数接收一个组件,并返回一个组件,返回的组件内部包含原始组件和对原始组件的props增加router功能。