装饰器

73 阅读5分钟

本篇讲述新版装饰器,也就是标准语法

TS早期就支持装饰器,ESCMAScript标准通过的语法标准与早期TS支持的语法有较大差异 在TS 5.0版本同时支持两种装饰器语法,标准语法可以直接使用,传统语法需要打开--experimentalDecorators编译参数

TS5.0语法vscode提示不支持,更新VScode版本即可,VScode安装包内内置有TS,路径为·Microsoft VS Code\resources\app\extensions\node_modules\typescript\lib·,直接更改该目录下的TS文件为5.0版本的也可以

简介

装饰器的语法特征 (1)第一个字符(或者说前缀)是@,后面是一个表达式。 (2)@后面的表达式,必须是一个函数(或者执行后可以得到一个函数)。 (3)这个函数接受所修饰对象的一些相关值作为参数。 (4)这个函数要么不返回值,要么返回一个新对象取代所修饰的目标对象。

装饰器是一种特殊类型的声明, 可以附加到类声明、方法、访问符、属性或参数上、在编译时执行只执行一次 装饰器可以为代码添加新功能而不影响代码的结构和逻辑,使代码可读性,可维护性和扩展性提高

unction simpleDecorator(value: any, context: any) {
    console.log(`hi, this is ${context.kind} ${context.name}`);
    return value;
}

@simpleDecorator
class A { } // "hi, this is class A"

如上装饰器simpleDecorator对类A进行装饰,无论A有没有实例,都会执行

装饰器的结构

装饰器函数的类型定义如下

type Decorator = (
    value: DecoratedValue,
    context: {
        kind: string;
        name: string | symbol;
        addInitializer?(initializer: () => void): void;
        static?: boolean;
        private?: boolean;
        access: {
            get?(): unknown;
            set?(value: unknown): void;
        };
        metadata: DecoratorMetadata;
    }
) => void | ReplacementValue;

装饰器函数接收value(装饰对象)和context(上下文对象)两个参数,其中TS提供原生接口ClassMethodDecoratorContext描述上下文对象

function decorator(value: any, context: ClassMethodDecoratorContext) {
    // ...
}
  • context对象属性,根据装饰对象不同而不同,其中只有kind和name必有,其余均是可选
  • kind:字符串,表示修饰对象的类型,取值为 class | method | getter | setter | field | accessor
  • name: 字符串或者Symbol值,装饰对象的名字,如类名、属性名等
  • addInitializer(): 函数,添加类的初始化逻辑,无返回值
  • private: boolean,所装饰的对象是否为类的私有成员
  • static:布尔值,表示所装饰的对象是否为类的静态成员。
  • access:一个对象,包含了某个值的 get 和 set 方法。
  • metadata: 装饰器元数据,元数据: 用于描述数据的数据,5.2版本新增参考文字

根据kind,标准装饰器可分为类装饰器、方法装饰器、属性装饰器、getter\setter装饰器、accessor装饰器

类装饰器

type ClassDecorator = (
    value: Function,
    context: {
        readonly kind: 'class';
        readonly name: string | undefined;
        addInitializer(initializer: () => void): void;
        readonly metadata: DecoratorMetadata;
    }
) => Function | void;

类装饰器接受两个参数,value为类本身,context是上下文对象,其中kind为固定值class,metadata是装饰器元数据,任意装饰器都存在 类装饰器用于对类进行操作,可以不返回值,也可以返回函数替代当前类的构造方法,还可以返回新的类替代原来的类

通过装饰器对类添加方法

nterface IGreeter {
    greet: () => void
}
interface User extends IGreeter { }

function Greeter(value: any, context: any) {
    // value.prototype.greet = function () {
    //     console.log('你好');
    // };
    return class extends value {
        greet() {
            console.log('你好')
        }
    }
}

@Greeter
class User { }

let u = new User();
u.greet(); // "你好"

通过装饰器给类添加方法会有TS类型检测问题,可以使用接口技巧修复

返会新的类替代原来装饰的类

interface ICount {
    count: number
}
interface MyClass extends ICount { }
function countInstances(value: any, context: any) {
    let instanceCount = 0;
    return class extends value {
        count: number
        constructor(...args: any[]) {
            super(...args);
            instanceCount++;
            this.count = instanceCount;
        }
    };
}
@countInstances
class MyClass { }

const inst1 = new MyClass();
inst1 instanceof MyClass // true
inst1.count // 1

contextaddInitializer方法用来定义类的初始化函数,在类完全定义结束后执行

// 为类注册指定名称的自定义HTML元素
function customElement(name: string) {
    return <Input extends new (...args: any) => any>(
        value: Input,
        context: ClassDecoratorContext
    ) => {
        context.addInitializer(function () {
            customElements.define(name, value);
        });
    };
}
@customElement("hello-world")
class MyComponent extends HTMLElement {
    constructor() {
        super();
    }
    connectedCallback() {
        this.innerHTML = `<h1>Hello World</h1>`;
    }
}

方法装饰器

type ClassMethodDecorator = (
    value: Function,
    context: {
        readonly kind: 'method';
        readonly name: string | symbol;
        readonly static: boolean;
        readonly private: boolean;
        readonly access: { get: () => unknown };
        addInitializer(initializer: () => void): void;
        readonly metadata: DecoratorMetadata;
    }
) => Function | void;
  • kind:值固定为字符串method,表示当前为方法装饰器。
  • name:所装饰的方法名,类型为字符串或 Symbol 值。
  • static:布尔值,表示是否为静态方法。
  • private:布尔值,表示是否为私有方法。
  • access:对象,包含了方法的存取器,但是只有get()方法用来取值,没有set()方法进行赋值。
  • addInitializer():为方法增加初始化函数。

方法装饰器会改写类的原始方法,如果方法装饰器返回一个新的函数,会替代所装饰的函数

unction replaceMethod(value: any, context: any) {
    return function () {
        // @ts-ignore
        return `How are you, ${(this).name}?`;
    }
}
class Person {
    name: string
    constructor(name: string) {
        this.name = name;
    }
    @replaceMethod
    hello() {
        return `Hi ${this.name}!`;
    }
}
const robin = new Person('Robin');
console.log(robin.hello()) // 'How are you, Robin?'

利用方法装饰器,对类的方法进行延迟执行

function delay(milliseconds: number = 0) {
    return function (value: any, context: ClassMethodDecoratorContext) {
        if (context.kind === "method") {
            return function (...args: any[]) {
                setTimeout(() => {
                    // @ts-ignore
                    value.apply(this, args);
                }, milliseconds);
            };
        }
    };
}

class Logger {
    @delay(3000)
    log(msg: string) {
        console.log(`${msg}`);
    }
}

let logger = new Logger();
#### logger.log("Hello World");

属性装饰器

type ClassFieldDecorator = (
    value: undefined,
    context: {
        readonly kind: 'field';
        readonly name: string | symbol;
        readonly static: boolean;
        readonly private: boolean;
        readonly access: { get: () => unknown, set: (value: unknown) => void };
        addInitializer(initializer: () => void): void;
        readonly metadata: DecoratorMetadata;
    }
) => (initialValue: unknown) => unknown | void;

属性装饰器的第一个参数为undefined,不能从value获取所装饰属性的值 属性装饰器可以不返回值,但是返回值的话需要返回函数,该函数会自动执行,用于对装饰属性初始化,参数为装饰属性的初始值,返回值为最终值

function logged(value: undefined, context: ClassFieldDecoratorContext) {
    const { kind, name } = context;
    if (kind === 'field') {
        return function (initialValue: string) {
            console.log(`initializing ${String(name)} with value ${initialValue}`);
            return initialValue + '_';
        };
    }
}

class Color {
    @logged name = 'green';
}

const color = new Color(); // "initializing name with value green"
color.name // green_

getter、setter装饰器

assess略有不同,getter装饰器只有get,setter装饰器只有set 这两个装饰器要么不返回值,返回的话必须返回函数,取代原有的取值器或者存值器

type ClassGetterDecorator = (
    value: Function,
    context: {
        readonly kind: 'getter';
        readonly name: string | symbol;
        readonly static: boolean;
        readonly private: boolean;
        readonly access: { get: () => unknown };
        addInitializer(initializer: () => void): void;
    }
) => Function | void;

type ClassSetterDecorator = (
    value: Function,
    context: {
        readonly kind: 'setter';
        readonly name: string | symbol;
        readonly static: boolean;
        readonly private: boolean;
        readonly access: { set: (value: unknown) => void };
        addInitializer(initializer: () => void): void;
    }
) => Function | void;

accessor 装饰器

accessor修饰符是装饰器语法引入的新的属性修饰符,作用是为类属性自动生成取值器和存值器 使用时需要将tsconfig的target改为ES2015及以上版本才行

class C {
    accessor x = 1;
}
// 等同于
class C2 {
    #x = 1;
    get x() {
        return this.#x;
    }
    set x(val) {
        this.#x = val;
    }
}

accessor还可以与静态属性和私有属性一起使用

类型定义

type ClassAutoAccessorDecorator = (
    value: {
        get: () => unknown;
        set: (value: unknown) => void;
    },
    context: {
        readonly kind: "accessor";
        readonly name: string | symbol;
        readonly access: { get(): unknown, set(value: unknown): void };
        readonly static: boolean;
        readonly private: boolean;
        addInitializer(initializer: () => void): void;
    }
) => {
    get?: () => unknown;
    set?: (value: unknown) => void;
    init?: (initialValue: unknown) => unknown;
} | void;

装饰器参数value,是包含get()和set()的对象,可以不返回值,也可以反悔新的对象取代原有的set()、get()方法,还可以反悔init方法,用于改变私有属性的值

// 装饰器@logged3为x的存值器和取值器加上日志输出

class C3 {
    @logged3 accessor x = 1;
}
function logged3(value: any, { kind, name }: { kind: string, name: string }) {
    if (kind === "accessor") {
        let { get, set } = value;
        return {
            get() {
                console.log(`getting ${name}`);
                return get.call(this);
            },
            set(val: number) {
                console.log(`setting ${name} to ${val}`);
                return set.call(this, val);
            },
            init(initialValue: number) {
                console.log(`initializing ${name} with value ${initialValue}`);
                return initialValue;
            }
        };
    }
}
let c3 = new C3();
c3.x; // getting x
c3.x = 123; // setting x to 123

装饰器输出顺序

装饰器执行分两个阶段 (1)评估(evaluation):计算@符号后面的表达式的值,得到的应该是函数。 (2)应用(application):将评估装饰器后得到的函数,应用于所装饰对象。 应用装饰器的顺序依次为方法装饰器、属性装饰器、类装饰器

function d(str: string) {
    console.log(`评估 @d(): ${str}`);
    return (
        value: any, context: any
    ) => console.log(`应用 @d(): ${str}`);
}
function log(str: string) {
    console.log(str);
    return str;
}
@d('类装饰器')
class T {
    @d('静态属性装饰器')
    static staticField = log('静态属性值');

    @d('原型方法')
    [log('计算方法名')]() { }

    @d('实例属性')
    instanceField = log('实例属性值');

    @d('静态方法装饰器')
    static fn() { }
}

// 评估 @d(): 类装饰器
// 评估 @d(): 静态属性装饰器
// 评估 @d(): 原型方法
// 计算方法名
// 评估 @d(): 实例属性
// 评估 @d(): 静态方法装饰器
// 应用 @d(): 静态方法装饰器
// 应用 @d(): 原型方法
// 应用 @d(): 静态属性装饰器
// 应用 @d(): 实例属性
// 应用 @d(): 类装饰器
// 静态属性值

评估时,按照出现的顺序执行,一个方法火属性有多个装饰器,内层的装饰器先执行,然后是外层装饰器