TypeScript:装饰器

一、什么是装饰器

装饰器是一种特殊类型的声明,它能够被附加到类声明方法属性或者参数上,

装饰期语法

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

function logController(target: any) { //target:运行时,被装饰的声明的信息Person的构造函数
    console.log('log')
}

 //@logController:  @ + expression
 //运行时被调用,被装饰的声明信息做为参数传入
 @logController class Person { 
    name: string = ''
    age: number = 0
    constructor(name: string, age: number) {
        this.name = name
        this.age = age
    }
}

const p = new Person('张三', 18) // log

开启装饰器

装饰器目前还是一项实验性特性,若要启用实验性的装饰器特性,必须tsconfig.json里启用experimentalDecorators编译器选项

// tsconfig.json
{
    "compilerOptions": {
        "experimentalDecorators": true // 开启装饰器
     }
}

常见装饰器:

类装饰器属性装饰器方法装饰器参数装饰器

装饰器的写法:

分为普通装饰器(无法传参)和 装饰器工厂(可以传参)

二、装饰器的写法

  • 普通装饰器
    interface Person {
        name: string
        age: string
    }
    
    function enhancer(target: any) {
        target.prototype.name = '金色小芝麻'
        target.prototype.age = '18'
    }
    
    @enhancer class Person { //@enhancer 普通装饰器
        constructor(){}
        
        print(){
            console.log(this.name); //金色小芝麻
            console.log(this.age);  //18
        }
    }
    
    const person = new Person()
    person.print()
  • 装饰器工厂

    interface Person {
        name: string
        age: number
    }
    
    // 利用函数柯里化解决传参问题, 向装饰器传入一些参数,也可以叫 参数注解
    function enhancer(name: string) {
        return function enhancer(target: any) {
            target.prototype.name = name // 这个 name 就是装饰器的元数据,外界传递进来的参数
            target.prototype.age = 18
        }
    }
    
    //在使用装饰器的时候, 为其指定元数据
    @enhancer('小芝麻') class Person {
        constructor() { }
    }

三、装饰器的分类

1. 类装饰器

类装饰器在类声明之前声明(紧靠着类声明),用来监视修改或者替换类定义

类装饰器不能用在声明文件中( .d.ts),也不能用在任何外部上下文中(比如declare的类)。
类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。
如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。

类装饰器的参数有且只有一个:

target:类的构造函数。

interface Person {
    name: string
    age: string
}
function enhancer(target: any) {
    target.prototype.name = '金色小芝麻'
    target.prototype.age = '18'
}

@enhancer class Person {
    constructor() { }
}
let p = new Person()
console.log(p.age) // 18

2. 属性装饰器

属性装饰器用来装饰属性
属性装饰器表达式会在运行时当做函数被调用,传入下列两个参数

  • 第一个参数(target): 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  • 第二个参数(propertyKey): 是属性的名称
function enhancer(target:any, propertyKey: string) {
    console.log(target); //{}
    console.log("key: "+propertyKey); //key: name
}

class Person {
    @enhancer name: string;
    constructor() {
        this.name = '金色小芝麻'
    }
}

 

3. 方法装饰器

方法装饰器用来装饰方法

  • 第一个参数(target): 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  • 第二个参数(propertyKey): 是方法的名称
  • 第三个参数(descriptor): 是方法的描述 修饰方法
function enhancer(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    // target 如果装饰的是个普通属性的话,那么这个 target 指向类的原型 Person.prototype
    console.log(target); // Person { getName: [Function] }
    console.log("key " + propertyKey); // key getName
    console.log("desc " + JSON.stringify(descriptor)); // {"writable":true,"enumerable":true,"configurable":true}
};

class Person {
    name: string;

    constructor() {
        this.name = '金色小芝麻';
    }

    @enhancer getName() {
        return 'getName';
    }
}

const user = new Person();
user.getName = function () {
    return '金色小芝麻'
}
console.log(user.getName()); // '金色小芝麻'

修饰静态方法

// 声明装饰器修饰静态方法
function enhancer(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    // target 装饰的是一个类的属性static,那么这个 target 指向类的定义
    console.log(target); // [Function: Person] { getAge: [Function] }
    console.log("key " + propertyKey); // key getAge
    console.log("desc " + JSON.stringify(descriptor)); // {"writable":true,"enumerable":true,"configurable":true}
};

class Person {
    age: number = 18;
    constructor() { }
    @enhancer static getAge() {
        return 'static getAge';
    }
}

const user = new Person();
Person.getAge = function () {
    return '你好啊!'
}

console.log(Person.getAge()) // 你好啊!

4. 参数装饰器

参数装饰器用来装饰参数

  • 第一个参数(target): 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  • 第二个参数(propertyKey): 成员的名字
  • 第三个参数(paramIndex): 参数在函数参数列表中的索引
function enhancer(target: any, propertyKey: string, parameterIndex: number) {
    console.log(target); // Person { getName: [Function] }
    console.log("key " + propertyKey); // key getName
    console.log("index " + parameterIndex); // index 0
};

class Person {
    name: string;
    constructor() {
        this.name = '你好啊!';
    }
    getName(@enhancer name: string) {
        return name
    }
}

const user = new Person();
user.name = '金色小芝麻'
console.log(user.name) // '金色小芝麻'

5. 访问器装饰器

访问器装饰器作业用于访问器上,接收3个参数:

  • 第一个参数(target): 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  • 第二个参数(propertyKey): 方法名。
  • 第三个参数(descriptor): 属性描述符。
const getSetDecorator = (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
    console.log(target, propertyKey, descriptor)
}

class Person {
    private _name: string = ''
    private _age: number = 0

    constructor(name: string, age: number) {
        this._name = name
        this._age = age
    }

    @getSetDecorator get name() {
        return this._name
    }

    set name(name: string) {
        this._name = name
    }
}

const p = new Person('张三', 18)

TypeScript不允许同时装饰一个成员的getset访问器,所以上面的set访问器不能够再添加装饰器,否则会报错。

6. 装饰器执行顺序

  • 属性方法先执行,谁先写 先执行谁
  • 方法的时候, 先参数在方法,而且一定会在一起
  • 最后是类
  • 如果同类型,先执行离类近的

四、实战例子

请求loading

在平时的业务开发中,我们往往会在请求接口数据时添加loading:

import { Loading } from 'element-ui'
export default {
    methods: {
        async fetchData() {
            const loading = Loading.service()
            try {
                await api.getList()
            } finally {
                loading.close() // 关闭loading
            }
        }
    }
}

通过装饰器实现后的样子:

export default {
    methods: {
        @loading() async fetchData() {
            await api.getList()
        }
    }
}

装饰器实现代码:

import { Loading } from 'element-ui'

/**
 * 装饰器:loading
 */
export const loading = (text = '加载中') => {
  return (target, name, descriptor) => {
    const fn = descriptor.value
    descriptor.value = async (...rest) => {
      const loading = Loading.service({
        text
      })
      try {
        return await fn.apply(this, rest)
      } finally {
        loading.close()
      }
    }
  }
}

确认弹窗

在进行删除操作时我们一般会弹出确认弹窗,确认弹窗的代码一般也都很固定,因此我们也可以写一个装饰器来简化这个过程。

import { MessageBox } from 'element-ui'
/**
 * 装饰器:确认弹窗
 */
export const confirm = (message) => {
  return (target, name, descriptor) => {
    const fn = descriptor.value
    descriptor.value = async (...rest) => {
      await MessageBox.confirm(message)
      fn.apply(this, rest)
    }
  }
}
// 使用装饰器
export default {
    methods: {
        @confirm('确认删除吗?') async onDelete() {
            await api.deleteUser()
        }
    }
}

以上两个例子只是装饰器应用于实际开发中的冰山一角,对装饰器感兴趣的朋友可以多深入了解并实践,实现更复杂的应用场景。

五、装饰器的原理

我们以下列 类装饰器 为例:

interface Person {
    name: string
    age: string
}

function enhancer(target: any) {
    target.xx = 'Person';
    target.prototype.name = '金色小芝麻'
    target.prototype.age = '18'
}

@enhancer class Person {
    constructor() { }
}

let p = new Person()

编译结果为:

 

我们先把装饰器的实现这部分代码拿出来稍微整理一下代码如下:

var __decorate =
    // 当前上下文是否有 __decorate 这个函数,如果有就返回这个函数,如果没有就定义一个
    //( this && this.__decorate 的作用 :为了避免重复声明)
    (this && this.__decorate) || function (decorators, target, key, desc) {
        /**
         * decorators: 装饰器数组, 
         * target: 构造函数, 
         * key: 方法名称,
         * desc: 方法描述
         */
        var c = arguments.length, // c: 当前参数的个数,此例中我们只传了两个参数,所以 c = 2 
            r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, // c < 3 所以 r = target(构造函数)
            d;
        // Reflect 详解:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect
        // 如果系统支持反射,则直接使用Reflect.decorate(decorators,target, key, desc)方法。
        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]) { // 给 d 赋值为 装饰器数组中的每一项 并且 不为 undefined 
                    // c < 3 执行 d(r) 
                    r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
                }
            }
        }
        return c > 3 && r && Object.defineProperty(target, key, r), r;
        // return c > 3 && r && Object.defineProperty(target, key, r), r; 
        // 等价于 =>
        // if(c > 3 && r) { 
        //    Object.defineProperty(target, key, r) 
        // }
        // return r
    };
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
function enhancer(target) {
    // target 接收的实参是: r (是 构造函数 Person)
    target.xx = 'Person'; // 给 Person 增加属性 
    target.prototype.name = '金色小芝麻'; // 给 Person 的原型 增加 name 属性 
    target.prototype.age = '18'; // 给 Person 的原型 增加 age 属性 
}
let Person = class Person {
    constructor() { }
};
Person = __decorate(
    // 传入两个实参,1、当前类使用的装饰器数组, 2、当前构造函数
    [enhancer, __metadata("design:paramtypes", [])], Person
);
let p = new Person();

执行步骤

1、定义一个 __decorate 函数(实现装饰器)

2、元数据我们暂时不考虑

3、定义一个 enhancer 函数

4、定义一个 Person 的 构造函数

5、给 Person 重新赋值

__decorate([ enhancer, __metadata("design:paramtypes", [])], Person);

 

posted on 2024-04-08 20:20  梁飞宇  阅读(46)  评论(0)    收藏  举报