• 装饰者模式和TypeScript装饰器


    导读

    本文主要为三方面的内容:

    1. 装饰者模式的概念和使用
    2. Typescript装饰器的使用、执行顺序
    3. 编译后的源码分析

    学习的目的是对装饰者模式模式有进一步的理解,并运用在自己的项目中;对TypeScript装饰器的理解,更好的使用装饰器,例如在 nodejs web 框架中、 vue-property-decorator 中,或者是自定义装饰器,能熟练运用并掌握其基本的实现原理。

    装饰者模式介绍

    装饰者模式(Decorator Pattern)也称为装饰器模式,在不改变对象自身的基础上,动态增加额外的职责。属于结构型模式的一种。

    使用装饰者模式的优点:把对象核心职责和要装饰的功能分开了。非侵入式的行为修改。

    举个例子来说,原本长相一般的女孩,借助美颜功能,也能拍出逆天的颜值。只要善于运用辅助的装饰功能,开启瘦脸,增大眼睛,来点磨皮后,咔嚓一拍,惊艳无比。

    经过这一系列叠加的装饰,你还是你,长相不增不减,却能在镜头前增加了多重美。如果你愿意,还可以尝试不同的装饰风格,只要装饰功能做的好,你就能成为“百变星君”。

    可以用代码表示,把每个功能抽象成一个类:

    // 女孩子
    class Girl {
      faceValue() {
        console.log('我原本的脸')
      }
    }
    
    class ThinFace  {
      constructor(girl) {
        this.girl = girl;
      }
      faceValue() {
        this.girl.faceValue();
        console.log('开启瘦脸')
      }
    }
    
    class IncreasingEyes  {
      constructor(girl) {
        this.girl = girl;
      }
      faceValue() {
        this.girl.faceValue();
        console.log('增大眼睛')
      }
    }
    
    let girl = new Girl();
    girl = new ThinFace(girl);
    girl = new IncreasingEyes(girl);
    
    // 闪瞎你的眼
    girl.faceValue(); // 

    从代码的表现来看,将一个对象嵌入到另一个对象中,相当于通过一个对象对另一个对象进行包装,形成一条包装链。调用后,随着包装的链条传递给每一个对象,让每个对象都有处理的机会。

    这种方式在增加删除装饰功能上都有极大的灵活性,假如你有勇气展示真实的脸,去掉瘦脸的包装即可,这对其他功能毫无影响;假如要增加磨皮,再来个功能类,继续装饰下去,对其他功能也无影响,可以并存运行。

    在 JavaScript 中增加小功能使用类,显的有点笨重,JavaScript 的优点是灵活,可以使用对象来表示:

    let girl = {
      faceValue() {
        console.log('我原本的脸')
      }
    }
    function thinFace() {
      console.log('开启瘦脸')
    }
    function IncreasingEyes() {
      console.log('增大眼睛')
    }
    
    girl.faceValue = function(){
      const originalFaveValue = girl.faceValue;  // 原来的功能
      return function() {
        originalFaveValue.call(girl);
        thinFace.call(girl);
      }
    }()
    girl.faceValue = function(){
      const originalFaveValue = girl.faceValue;  // 原来的功能
      return function() {
        originalFaveValue.call(girl);
        IncreasingEyes.call(girl);
      }
    }()
    
    girl.faceValue();

    在不改变原来代码的基础上,通过先保留原来函数,重新改写,在重写的代码中调用原来保留的函数。

    用一张图来表示装饰者模式的原理:

    https://cdn.nlark.com/yuque/0/2020/jpeg/150099/1587202888193-39950b89-6763-4932-9122-c2ec93962e20.jpeg

    从图中可以看出来,通过一层层的包装,增加了原先对象的功能。

    TypeScript中的装饰器

    TypeScript 中的装饰器使用 @expression 这种形式,expression 求值后为一个函数,它在运行时被调用,被装饰的声明信息会被做为参数传入。

    Javascript规范里的装饰器目前处在 建议征集的第二阶段,也就意味着不能在原生代码中直接使用,浏览器暂不支持。

    可以通过 babel 或 TypeScript 工具在编译阶段,把装饰器语法转换成浏览器可执行的代码。(最后会有编译后的源码分析)

    以下主要讨论 TypeScript 中装饰器的使用。

    TypeScript 中的装饰器可以被附加到类声明、方法、 访问符(getter/setter)、属性和参数上。

    开启对装饰器的支持,命令行 编译文件时:

    tsc --target ES5 --experimentalDecorators test.ts

    配置文件 tsconfig.json

    {
        "compilerOptions": {
            "target": "ES5",
            "experimentalDecorators": true
        }
    }

    装饰器的使用

    装饰器实际上就是一个函数,在使用时前面加上 @ 符号,写在要装饰的声明之前,多个装饰器同时作用在一个声明时,可以写一行或换行写:

    // 换行写
    @test1
    @test2
    declaration
    
    //写一行
    @test1 @test2 ...
    declaration

    定义 face.ts 文件:

    function thinFace() {
      console.log('开启瘦脸')
    }
    
    @thinFace
    class Girl {
    }

    编译成 js 代码,在运行时,会直接调用 thinFace 函数。这个装饰器作用在类上,称之为类装饰器。

    如果需要附加多个功能,可以组合多个装饰器一起使用:

    function thinFace() {
      console.log('开启瘦脸')
    }
    function IncreasingEyes() {
      console.log('增大眼睛')
    }
    
    @thinFace
    @IncreasingEyes
    class Girl {
    }
    

    多个装饰器组合在一起,在运行时,要注意,调用顺序是 从下至上 依次调用,正好和书写的顺序相反。例子中给出的运行结果是:

    '增大眼睛'
    '开启瘦脸'

    如果你要在一个装饰器中给类添加属性,在其他的装饰器中使用,那就要写在最后一个装饰器中,因为最后写的装饰器最先调用。

    装饰器工厂

    有时需要给装饰器传递一些参数,这要借助于装饰器工厂函数。装饰器工厂函数实际上就是一个高阶函数,在调用后返回一个函数,返回的函数作为装饰器函数。

    function thinFace(value: string){
      console.log('1-瘦脸工厂方法')
      return function(){
        console.log(`4-我是瘦脸的装饰器,要瘦脸${value}`)
      }
    }
    function IncreasingEyes(value: string) {
      console.log('2-增大眼睛工厂方法')
      return function(){
        console.log(`3-我是增大眼睛的装饰器,要${value}`)
      }
    }
    
    @thinFace('50%')
    @IncreasingEyes('增大一倍')
    class Girl {
    }

    @ 符号后为调用工厂函数,依次从上到下执行,目的是求得装饰器函数。装饰器函数的运行顺序依然是从下到上依次执行。

    运行的结果为:

    1-瘦脸工厂方法
    2-增大眼睛工厂方法
    3-我是增大眼睛的装饰器,要增大一倍
    4-我是瘦脸的装饰器,要瘦脸50%

    总结一下:

    1. 写了工厂函数,从上到下依次执行,求得装饰器函数。
    2. 装饰器函数的执行顺序是 从下到上 依次执行。

    类装饰器

    作用在类声明上的装饰器,可以给我们改变类的机会。在执行装饰器函数时,会把类构造函数传递给装饰器函数。

    function classDecorator(value: string){
      return function(constructor){
        console.log('接收一个构造函数')
      }
    }
    
    function thinFace(constructor){
      constructor.prototype.thinFaceFeature = function() {
        console.log('瘦脸功能')
      }
    }
    
    @thinFace
    @classDecorator('类装饰器')
    class Girl {}
    
    let g = new Girl();
    
    g.thinFaceFeature(); // '瘦脸功能'

    上面的例子中,拿到传递构造函数后,就可以给构造函数原型上增加新的方法,甚至也可以继承别的类。

    方法装饰器

    作用在类的方法上,有静态方法和原型方法。作用在静态方法上,装饰器函数接收的是类构造函数;作用在原型方法上,装饰器函数接收的是原型对象。
    这里拿作用在原型方法上举例。

    
    function methodDecorator(value: string, Girl){
      return function(prototype, key, descriptor){
        console.log('接收原型对象,装饰的属性名,属性描述符', Girl.prototype === prototype)
      }
    }
    
    function thinFace(prototype, key, descriptor){
      // 保留原来的方法逻辑
      let originalMethod = descriptor.value;
      // 改写,增加逻辑,并执行原有逻辑
      descriptor.value = function(){
        originalMethod.call(this);  // 注意修改this的指向
        console.log('开启瘦脸模式')
      }
    }
    
    class Girl {
    
      @thinFace
      @methodDecorator('方式装饰器', Girl)
      faceValue(){
        console.log('我是原本的面目')
      }
    }
    
    let g = new Girl();
    
    g.faceValue();

    从代码中可以看出,装饰器函数接收三个参数,原型对象、方法名、描述对象。对描述对象陌生的,可以参考 这里;

    要增强功能,可以先保留原来的函数,改写描述对象的 value 为另一函数。

    当使用 g.faceValue() 访问方法时,访问的就是描述对象 value 对应的值。

    在改写的函数中增加逻辑,并执行原来保留的原函数。注意原函数要用 call 或 apply 将 this 指向原型对象。

    属性装饰器

    作用在类中定义的属性上,这些属性不是原型上的属性,而是通过类实例化得到的实例对象上的属性。

    装饰器同样会接受两个参数,原型对象,和属性名。而没有属性描述对象,为什么呢?这与TypeScript是如何初始化属性装饰器的有关。 目前没有办法在定义一个原型对象的成员时描述一个实例属性。

    function propertyDecorator(value: string, Girl){
      return function(prototype, key){
        console.log('接收原型对象,装饰的属性名,属性描述符', Girl.prototype === prototype)
      }
    }
    
    function thinFace(prototype, key){
      console.log(prototype, key)
    }
    
    class Girl {
      @thinFace
      @propertyDecorator('属性装饰器', Girl)
      public age: number = 18;
    }
    
    let g = new Girl();
    
    console.log(g.age); // 18

    其他装饰器的写法

    下面组合多个装饰器写在一起,出了上面提到的三种,还有 访问符装饰器、参数装饰器。这些装饰器在一起时,会有执行顺序。

    
    function classDecorator(value: string){
      console.log(value)
      return function(){}
    }
    function propertyDecorator(value: string) {
      console.log(value)
      return function(){
        console.log('propertyDecorator')
      }
    }
    function methodDecorator(value: string) {
      console.log(value)
      return function(){
        console.log('methodDecorator')
      }
    }
    function paramDecorator(value: string) {
      console.log(value)
      return function(){
        console.log('paramDecorator')
      }
    }
    function AccessDecorator(value: string) {
      console.log(value)
      return function(){
        console.log('AccessDecorator')
      }
    }
    function thinFace(){
      console.log('瘦脸')
    }
    function IncreasingEyes() {
      console.log('增大眼睛')
    }
    
    
    @thinFace
    @classDecorator('类装饰器')
    class Girl {
      @propertyDecorator('属性装饰器')
      age: number = 18;
      
      @AccessDecorator('访问符装饰器')
      get city(){}
    
      @methodDecorator('方法装饰器')
      @IncreasingEyes
      faceValue(){
        console.log('原本的脸')
      }
    
      getAge(@paramDecorator('参数装饰器') name: string){}
    }

    运行了这段编译后的代码,会发现这些访问器的顺序是,属性装饰器 -> 访问符装饰器 -> 方法装饰器 -> 参数装饰器 -> 类装饰器。

    更详细的用法可以参考官网文档:https://www.tslang.cn/docs/handbook/decorators.html#decorator-factories

    广州设计公司https://www.houdianzi.com 我的007办公资源网站https://www.wode007.com

    装饰器运行时代码分析

    装饰器在浏览器中不支持,没办法直接使用,需要经过工具编译成浏览器可执行的代码。

    分析一下通过工具编译后的代码。

    生成 face.js 文件:

    tsc --target ES5 --experimentalDecorators face.ts

    打开 face.js 文件,会看到一段被压缩后的代码,可以格式化一下。

    先看这段代码:

    __decorate([
        propertyDecorator('属性装饰器')
    ], Girl.prototype, "age", void 0);
    __decorate([
        AccessDecorator('访问符装饰器')
    ], Girl.prototype, "city", null);
    __decorate([
        methodDecorator('方法装饰器'),
        IncreasingEyes
    ], Girl.prototype, "faceValue", null);
    __decorate([
        __param(0, paramDecorator('参数装饰器'))
    ], Girl.prototype, "getAge", null);
    Girl = __decorate([
        thinFace,
        classDecorator('类装饰器')
    ], Girl);

    __decorate 的作用就是执行装饰器函数,从这段代码中能够看出很多信息,印证上面得到的结论。

    通过__decorate调用顺序,可以看出来,多个类型的装饰器一起使用时,顺序是,属性装饰器 -> 访问符装饰器 -> 方法装饰器 -> 参数装饰器 -> 类装饰器。

    调用了 __decorate 函数,根据使用的装饰器类型不同,传入的参数也不相同。

    第一个参数传入的都一样,为数组,这样确保和我们书写的顺序一致,每一项是求值后的装饰器函数,如果写的是 @propertyDecorator() 则一上来就执行,得到装饰器函数,这跟上面分析的一致。

    类装饰器会把类作为第二个参数,其他的装饰器,把原型对象作为第二个参数,属性名作为第三个,第四个是 null 或 void 0void 0的值为undefined,也就等于没传参数

    要记住传给 __decorate 函数参数的个数和值,在深入到 __decorate 源码中, 会根据这些值来决定执行装饰器函数时,传入参数的多少。

    好,来看 __decorate 函数实现:

    // 已存在此函数,直接使用,否则自己定义
    var __decorate = (this && this.__decorate) ||
    // 接收四个参数: 
    //decorators存放装饰器函数的数组、target原型对象|类,
    //key属性名、desc描述(undefined或null)
    function(decorators, target, key, desc) {
      var c = arguments.length,
      // 拿到参数的个数
      r = c < 3 // 参数小于三个,说明是类装饰器,直接拿到类
        ? target
        : desc === null // 第四个参数为 null,则需要描述对象;属性装饰器传入是  void 0,没有描述对象。
            ? desc = Object.getOwnPropertyDescriptor(target, key) 
            : desc,
      d;
      // 如果提供了Reflect.decorate方法,直接调用;否则自己实现
      if (typeof Reflect === "object" && typeof Reflect.decorate === "function") 
        r = Reflect.decorate(decorators, target, key, desc);
      else 
        // 装饰器函数执行顺序和书写的顺序相反,从下至上 执行
        for (var i = decorators.length - 1; i >= 0; i--) 
          if (d = decorators[i]) // 拿到装饰器函数
              r = (c < 3 // 参数小于3个,说明是类装饰器,执行装饰器函数,直接传入类
                ? d(r) 
                : c > 3 // 参数大于三个,是方法装饰器、访问符装饰器、参数装饰器,则执行传入描述对象
                  ? d(target, key, r) 
                  : d(target, key) // 为属性装饰器,不传入描述对象
                ) || r;
    
      // 给被装饰的属性,设置得到的描述对象,主要是针对,方法、属性来说的
      /*** 
         * r 的值分两种情况,
         *  一种是通过上面的 Object.getOwnPropertyDescriptor 得到的值
         *  另一种,是装饰器函数执行后的返回值,作为描述对象。
         *      一般不给装饰器函数返回值。
        */
      return c > 3 && r && Object.defineProperty(target, key, r),r;
    };

    上面的参数装饰器,调用了一个函数为 __params

    var __param = (this && this.__param) || function (paramIndex, decorator) {
        return function (target, key) { decorator(target, key, paramIndex); }
    };

    目的是,要给装饰器函数传入参数的位置 paramIndex

    看了编译后的源码,相信会对装饰器的理解更深刻。

  • 相关阅读:
    C#序列化效率对比
    将聚合记录集逆时针和顺时针旋转90度(行列互换)
    Sql的行列转换
    log4net配置
    input框添加阴影效果
    WCF自定义地址路由映射(不用svc文件)
    Jquery对当前日期的操作(格式化当前日期)
    用JQuery获取输入框中的光标位置
    sublime text3安装后html:5+Tab不能快速生成html头部信息的解决办法
    js获取url传递参数,js获取url?号后面的参数
  • 原文地址:https://www.cnblogs.com/qianxiaox/p/13859330.html
Copyright © 2020-2023  润新知