Decorator装饰器是ES7的时候提案的特性,目前处于Stage 3候选阶段(2022年10月)。
装饰器简单来说就是修改类和类方法的语法糖,很多面向对象语言都有装饰器这一特性。
接上文,在JS中使用装饰器,本文介绍一下在TS中使用装饰器。
TypeScript已经将装饰器作为一项实验性特性支持了,我们可以直接通过修改配置开启装饰器特性。
tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true
}
}
一图了解TypeScript的语法。
@frozen
class Foo {
@throttle(500)
expensiveMethod() {}
}
类装饰器应用于类构造函数,可以用来监视、修改或替换类定义。
declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
返回值用于代替构造器。因此适合用于继承一个现有类并添加一些属性和方法。
type Consturctor = { new (...args: any[]): any };
function toString<T extends Consturctor>(BaseClass: T) {
return class extends BaseClass {
toString() {
return JSON.stringify(this);
}
};
}
@toString
class C {
public foo = "foo";
public num = 24;
}
console.log(new C().toString())
// -> {"foo":"foo","num":24}
可以用来监视、修改或替换一个属性的定义。
declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;
返回值被忽略。
function logParameter(target: Object, propertyKey: string) {
let value = target[propertyKey]
Object.defineProperty(target, propertyKey, {
set: function (newVal) {
value = newVal
console.log(`Set: ${propertyKey} => ${value}`);
},
get: function () {
console.log(`Get: ${propertyKey} => ${value}`);
return value
},
enumerable: true,
configurable: true
})
}
class Employee {
@logParameter
name: String;
}
const emp = new Employee();
emp.name = "Mohan Ram"; // Set: name => Mohan Ram
emp.name; // Get: name => Mohan Ram
可以用来监视、修改或者替换方法定义。
declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
返回值用于替换属性装饰器。
function writable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log(target)
console.log(propertyKey)
console.log(descriptor)
descriptor.writable = value
}
}
class Welcome {
user: string
constructor(user: string) {
this.user = user
}
@writable(false)
showWarn() {
return "Hello, " + this.user
}
}
const welcome = new Welcome('Peter')
welcome.showWarn = () => { // 报错不能修改只读的类成员
return '1'
}
可以用来监视、修改或替换一个访问器的定义。
注意 TypeScript 不允许同时装饰一个成员的 get 和 set 访问器。因此,如果想为一个成员的访问器添加装饰器,则必须添加在该成员在文档顺序上的第一个访问器前。因为装饰器应用于属性描述符时联合了 get 和 set 访问器,而不是分开声明的。
同方法装饰器。
返回值用于替换属性装饰器。
但属性装饰器的key不同: 方法装饰器的描述器的key为:
value
writable
enumerable
configurable
访问器装饰器的描述器的key为:
get
set
enumerable
configurable
function configurable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value
console.log(descriptor)
}
}
class Welcome {
user: String
constructor(user: String) {
this.user = user
}
@configurable(false)
set setUser(user: String) {
this.user = user
}
get getUser() {
return 'Hello, ' + this.user
}
}
const welcome = new Welcome('Peter')
console.log(welcome.getUser)
// {
// get: undefined,
// set: [Function: set setUser],
// enumerable: false,
// configurable: false
// }
// Hello, Peter
单独的参数装饰器能做的事情很有限,它一般都被用于记录可被其它装饰器使用的信息。
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;
返回值被忽略。
function required(target: any, key: string, index: number) {
console.log(target === A)
console.log(target === A.prototype)
console.log(key)
console.log(index)
}
class A {
saveData(@required name: string){} // 输出 false true name 0
}
当我们需要给装饰器传自定义参数时,需要构造一个装饰器工厂函数。
type Consturctor = { new (...args: any[]): any };
function testable(isTestable:boolean) {
return <T extends Consturctor>(BaseClass: T) => {
return class extends BaseClass {
isTestable:boolean = isTestable;
}
}
}
@testable(true)
class MyTestableClass {
isTestable:boolean
};
console.log((new MyTestableClass()).isTestable); // true
@testable(false)
class MyClass {
isTestable:boolean
};
console.log((new MyClass()).isTestable); // false
多个装饰器同时应用于一个对象时,装饰器工厂函数从上至下
开始执行,装饰器函数从下至上
开始执行。
function first() {
console.log("first(): factory evaluated");
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("first(): called");
};
}
function second() {
console.log("second(): factory evaluated");
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("second(): called");
};
}
class ExampleClass {
@first()
@second()
method() {}
}
// first(): factory evaluated
// second(): factory evaluated
// second(): called
// first(): called
为vue的添加@props
装饰器。
我们先来看下,正常情况下定义一个props的代码。
<template>
<div style="display: flex;">
{{msg}}
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
msg: {
type: String,
default: 'Hi',
},
},
});
</script>
未来简化声明props的代码,我们希望只通过@Prop({type: String, default: 'Hi'})
就完成props的声明。
这里我们直接用到了vue-property-decorator这个库。
下面是使用装饰器的写法。
import { Vue, Component, Prop } from 'vue-property-decorator'
@Component
export default class YourComponent extends Vue {
@Prop({ type: String, default: 'default value' }) readonly msg!: string
}
我们看一下@Prop
装饰器的语法。
简单过程就是获取装饰器参数里面的属性,赋值给vue类的props选项。
这里为了把vue组件转换成类语法,用到了vue-class-component这个库。
import Vue, { PropOptions } from 'vue'
import { createDecorator } from 'vue-class-component'
import { Constructor } from 'vue/types/options'
import { applyMetadata } from '../helpers/metadata'
/**
* 封装的处理props属性的装饰器
* @param options @Prop(options)装饰器内的options参数选项
* @return PropertyDecorator | void
*/
export function Prop(options: PropOptions | Constructor[] | Constructor = {}) {
return (target: Vue, key: string) => {
// 如果@Prop(options)的options不存在type属性,获取被装饰对象的元数据类型属性,赋值给options.type
applyMetadata(options, target, key)
// vue-class-component生成的options.props[key]赋值为@Prop的参数options。
createDecorator((componentOptions, k) => {
/**
* - componentOptions 是vue-class-component生成的options参数
* - k 是@Prop装饰器装饰的属性
* - options 是@Prop(options)装饰器的options参数
*/
;(componentOptions.props || ((componentOptions.props = {}) as any))[
k
] = options
})(target, key)
}
}