TypeScript 中的装饰器
什么是 Decorator
装饰器(Decorator)用来增强 JavaScript 类(class)的功能,许多面向对象的语言都有这种语法,ES 的这一语法提案目前仍处于 stage3 阶段。
简介
Decorator(装饰器)本质上是一个传入固定参数的函数,主要修饰类及类的成员变量。 装饰器使用 @
语法:
function Deco() {}
@Deco
class A {}
一般情况下我们并不会直接使用装饰器的方式,而是使用一种叫做 Decorator Factory
的形式:
function Deco(){
return () => {}
}
@Deco()
class A {}
跟第一种写法相比,不同的地方在于 Deco
函数内部又返回了一个函数,使用方式也从 @Deco
变成了函数调用 @Deco()
,后者的优点在于程序执行时,优先执行 Deco()
,然后通过 Deco
内部返回的函数作为装饰器的实际逻辑,这样,我们就可以通过入参来灵活使用装饰器。
装饰器的使用方式
装饰器的作用范围
根据 ECMAScript 规范,装饰器只能作用于:
- 类
- 类的属性
- 类的方法
- 属性存储器/访问符(accessor):getter、setter
但是在 TS 中又支持了参数装饰器,所以装饰器一共可以作用于5个部分。 先通过一段伪代码简单了解一下写法:
@Deco() // 1.类装饰器
class A {
@modifyName() // 2.参数装饰器
name!: string
@log() // 3.方法装饰器
method(){}
@addOne() // 4.属性存储器(访问符)装饰器
getter() {}
// setter() {}
handler(@CheckParam() input: string) { // 5.参数装饰器
console.log(input);
}}
下面来看下这几种的用法:
INFO
TS 中想要使用装饰器需要开启 experimentalDecorators
选项。
类装饰器
我们上面说到,装饰器的本质是一个传入固定参数的函数,那么类装饰器的入参就是类本身,而不是类的原型对象。因此我们可以通过类装饰器复写类的属性和方法,甚至可以通过装饰器返回一个新类。
给类 A
添加新的属性和方法:
const addMethod = (): ClassDecorator => {
return (target:any) => { // 添加到原型上
target.prototype.methodInProrotype = () => {
console.log('method in prototype')
} // 添加到类本身上
target.methodInClass = () => {
console.log('method in class')
}
}}
const addProperty = (value: string): ClassDecorator => {
return (target: any) => {
target.prototype.propertyInPrototype = value
target.propertyInClass = `static-${value}`
}}
@addMethod()
@addProperty('shinji')
class A {}
const a:any = new A()
console.log(a.methodInProrotype()) // method in prototype
console.log((A as any).methodInClass()) // method in class
console.log(a.propertyInPrototype) // shinji
console.log((A as any).propertyInClass) // static-shinji
方法装饰器
方法装饰器的固定参数分别是:类的原型、方法名以及方法的属性描述符(PropertyDescriptor),我们可以通过方法的属性描述符来控制方法的内部实现(value)、enumaerable、writable 等。
通过方法装饰秀,来统计实例方法的执行时间。
提供了两种写法,使用 Decorator Factory 的形式及不使用 Factory 的形式:
const calcRuntime = (): MethodDecorator => {
return (target, methodName, descriptor: TypedPropertyDescriptor<any>) => {
const originalMethod = descriptor.value
descriptor.value = function (...args: unknown[]){
const start = Date.now()
const res = originalMethod.apply(this, ...args)
const end = Date.now()
console.log(`${String(methodName)} Time: `,end- start, 'ms');
return res
}
}}
class A {
@calcRuntime()
billionLoop() {
for (let i=0;i<1000000;i++) {}
}
}
console.log(new A().billionLoop()) // "billionLoop Time: ", 2ms
const calcRuntime = (target:any, methodName: any, descriptor: any) => {
const originalMethod = descriptor.value
descriptor.value = function (...args: unknown[]){
const start = Date.now()
const res = originalMethod.apply(this, ...args)
const end = Date.now()
console.log(`${String(methodName)} Time: `,end- start,'ms');
return res
}}
class A {
@calcRuntime
billionLoop() {
for (let i=0;i<1000000;i++) {}
}}
new A().billionLoop()
需要特别注意:方法装饰器的 target 是类的原型而非类本身
属性装饰器
属性装饰器的固定入参只有:类的原型和属性名称。属性装饰器作用有限,但是我们依旧可以通过直接在类的原型上赋值的方式来修改属性。
const modifyNum = (): PropertyDecorator => {
return (target: any, propertyIdentifier) => {
target[propertyIdentifier] = 100
}}
class A {
@modifyNum()
num!: number
}
const a = new A()
console.log(a.num, 'num') // 100, num
参数装饰器
参数装饰器其中包括两部分:构造函数参数装饰器和方法参数装饰器,且他们的入参包括类的原型、参数所在的方法名及参数在函数中的索引(即第几个参数)。
const checkParam = ():ParameterDecorator => {
return (target: any, identifier, index) => {
console.log(target, identifier, index)
}
}
class A {
handle(@checkParam() num: number) {}
}
const a = new A()
a.handle(1) // A: {}, "handle", 0
访问符装饰器
访问符就是常见的get value(){}
与 set value(val){}
这样的方法。所以访问符装饰器本质上也是方法的装饰器,但是访问符装饰器是一种比较特殊的存在,即访问符装饰器只能作用于一对访问符getter/setter
其中之一上,即作用于 getter
就不能作用于 setter
,反之亦然。这是因为,不论你是装饰的是哪一个,装饰器入参中的属性描述符都会包括 getter 与setter 方法:
const setterDeco = (value: string): MethodDecorator => {
return (target: any, identifier, descriptor: any) => {
const originalSetter = descriptor.set
descriptor.set = (newValue: string) => {
const composed = `Raw: ${newValue}, Actual: ${value}-${newValue}`
originalSetter.call(A.prototype, composed);
console.log(`setterDeco: ${composed}`)
}
}}
class A {
_value!: string;
get value() {
return this._value;
}
@setterDeco('shinji')
set value(input: string) {
this._value = input;
}}
const a = new A();
a.value = 'ayanami'
// "setterDeco: Raw: ayanami, Actual: shinji-ayanami"
上面这段代码,我们在 a.value
的时候设置的值是 ayanami
,但是通过访问符装饰器对 setter
的方法进行了拦截,将值改为了 shinji-ayanami
但依旧可以通过 a.value
进行取值,取到的值就是拦截后修改的值,即 Raw: ayanami, Actual: shinji-ayanami
console.log(a.value) // Raw: ayanami, Actual: shinji-ayanami
装饰器的入参和返回值类型
根据上文简单总结一下:
- 类:(class) => ClassDecorator
- 方法:(class.prototype, identifier, descriptor) => MethodDecorator
- identifier: 装饰器修饰的方法名
- 访问符:(class.prototype, identifier, descriptor) => MethodDecorator
- identifier: 装饰器修饰的方法名
- 参数:(class.prototype, identifier, index) => ParameterDecorator
- identifier:装饰器修饰的参数所在的方法名
- 属性:(class.prototype, propertyIdentifier) => PropertyDecorator
- propertyIdentifier: 装饰器修饰的属性名
可以看出来,唯一相同的返回值类型就是:方法装饰器和访问符装饰器,本质上两者都是装饰方法的。
装饰器的执行机制
装饰器的执行机制包括三个部分:执行时机、执行原理和执行顺序。
执行时机
因为装饰器的本质就是一个函数,所以当一个类绑定了一个装饰器的时候,即使不主动实例化这个类,或者不去调用类的一些静态方法,装饰器也是会正常执行的,比如:
function Cls(): ClassDecorator {
return (target) => {
console.log("target:", target)
}
}
@Cls()
class A {}
// 无需 new A() 装饰器也是正常执行的,最终会打印:
// "target:", class A { }
执行原理
装饰器原理我们可以通过编译一段代码看一下代码最终的效果:
// 编译前
function Cls(): ClassDecorator {
return (target) => console.log(target, "cls")
}
function Method(): MethodDecorator {
return (target) => console.log(target, 'method')
}
function Prop(): PropertyDecorator {
return (target) => console.log(target, 'prop')
}
function Param(): ParameterDecorator {
return (target) => console.log(target, 'param')
}
@Cls()
class A {
constructor(@Param() init?: string) { }
@Prop()
prop: string = 'shinji'
@Prop()
static staticProp: string = 'static shinji'
@Method()
handler(@Param() input: string) {}
@Method()
static staticHandler(@Param() input: string) {}
}
const foo = new A()
// 编译后,省略一些代码
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
// 省略具体实现
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
function Cls() {
return (target) => console.log(target, "cls");
}
function Method() {
return (target) => console.log(target, 'method');
}
// ...
let A = class A {
constructor(init) {
this.prop = 'shinji';
}
handler(input) { }
static staticHandler(input) { }
};
A.staticProp = 'static shinji';
// 看这里!!!
__decorate([
Prop(),
__metadata("design:type", String)
], A.prototype, "prop", void 0);
__decorate([
Method(),
__param(0, Param()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", void 0)
], A.prototype, "handler", null);
__decorate([
Prop(),
__metadata("design:type", String)
], A, "staticProp", void 0);
__decorate([
Method(),
__param(0, Param()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", void 0)
], A, "staticHandler", null);
A = __decorate([
Cls(),
__param(0, Param()),
__metadata("design:paramtypes", [String])
], A);
const foo = new A();
编译后的代码首先定义了个一 __decorate
方法,这个方法的作用是通过传入的参数来判断到底是哪种 decorator(装饰器),然后执行对应的装饰器逻辑。我们再观察一下上面编译后的代码,每种 decorator 函数调用传入的参数是不同的:
装饰器类型 | 入参类型 |
---|---|
方法 | 类的原型 |
属性 | 类的原型 |
类 | 类本身 |
静态属性 | 类本身 |
静态方法 | 类本身 |
需要注意的是属性装饰器再应用时未被初始化(属性实例化后才会有值),所以无法获取到值,所以入参是 void 0
。
执行顺序
上面编译后的代码同样可以看到装饰器的执行顺序是:
- 实例属性
- 实例方法
- 实例方法的参数
- 静态属性
- 静态方法
- 静态方法的参数
- 类及类构造函数
说完执行顺序,再说一下应用顺序,之前有提到说到装饰器被绑定到类上的时候也是正常执行的,这其实是装饰器的一个执行过程,也就是上面说的执行顺序,当实例化这个类的时候,装饰器工厂求值后得到的表达式才开始真正的生效:
关于执行顺序与应用顺序,执行是装饰器求值得到最终装饰器表达式的过程,而应用则是最终装饰器逻辑代码执行的过程
const decoratorFactory = () => {
// 执行
return (target) => { /* 应用 */ }
}
装饰器的应用顺序与执行顺序稍有差别,其中方法参数装饰器会先于方法装饰器应用(__param(0, Param())
) 但实际上,实例属性、方法,静态属性、方法,这些装饰器的执行顺序取决于在类中定义的位置,调整一下定义顺序后,对应编译后的顺序也会改变,即先定义先执行。具体的顺序可以参考 typescript 官方给出的 [Decorator Evaluation](TypeScript: Documentation - Decorators (typescriptlang.org)) 我们可以通过一段代码,看下具体的执行顺序与应用顺序:
function excuteOrder(identifiery: any): any {
console.log(`${identifiery}: 执行`)
return (target:any) => console.log(`${identifiery}: 应用`)
}
@excuteOrder('class')
class A {
constructor(@excuteOrder('constructor') init?: string) { }
@excuteOrder('prop')
prop: string = 'shinji'
@excuteOrder('static prop')
static staticProp: string = 'static shinji'
@excuteOrder('instance method')
handler(@excuteOrder('instance method param') input: string) {}
@excuteOrder('static method')
static staticHandler(@excuteOrder('static method param') input: string) {}
}
// [LOG]: "prop: 执行"
// [LOG]: "prop: 应用"
// [LOG]: "instance method: 执行"
// [LOG]: "instance method param: 执行"
// [LOG]: "instance method param: 应用"
// [LOG]: "instance method: 应用"
// [LOG]: "static prop: 执行"
// [LOG]: "static prop: 应用"
// [LOG]: "static method: 执行"
// [LOG]: "static method param: 执行"
// [LOG]: "static method param: 应用"
// [LOG]: "static method: 应用"
// [LOG]: "class: 执行"
// [LOG]: "constructor: 执行"
// [LOG]: "constructor: 应用"
// [LOG]: "class: 应用"
上述 log 打印的顺序中类装饰是在最后应用的,也就是说在类装饰器应用的阶段,我们可以使用到方法或者其他装饰器标记的信息,比如给某个方法标记了 @deprecated
后,我们可以通过类装饰器把标记了 @deprecated
的方法进行报错处理等。
多个同类装饰器的执行顺序
直接看代码:
function First(): any {
console.log('First 执行')
return (target:any) => console.log('First 应用')
}
function Second(): any {
console.log('Second 执行')
return (target:any) => console.log('Second 应用')
}
function Third(): any {
console.log('Third 执行')
return (target:any) => console.log('Third 应用')
}
@First()
@Second()
@Third()
class A {}
// log:
// [LOG]: "First 执行"
// [LOG]: "Second 执行"
// [LOG]: "Third 执行"
// [LOG]: "Third 应用"
// [LOG]: "Second 应用"
// [LOG]: "First 应用"
像,实在是太像了,和洋葱模型简直一模一样。同样,方法中的参数装饰器也遵循这一机制。
以上就是 TypeScript 中装饰器相关的内容。