使用理解

在 JavaScript 中,装饰器的作用可以类比 Java 的注解。但其更多的类似于 AOP 语法糖,因为其本身不需要指定处理器,装饰器在定义时即可对目标进行环绕增强,可以有效地剥离出与业务无关的模板代码,减少冗余。其本质上是一个特定类型的函数,通过 @函数名 的方式使用。

装饰器类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
interface TypedPropertyDescriptor<T> {
enumerable?: boolean;
configurable?: boolean;
writable?: boolean;
value?: T;
get?: () => T;
set?: (value: T) => void;
}
declare type ClassDecorator = <TFunction extends Function>(
target: TFunction
) => TFunction | void;
declare type PropertyDecorator = (
target: Object,
propertyKey: string | symbol
) => void;
declare type MethodDecorator = <T>(
target: Object,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;
declare type ParameterDecorator = (
target: Object,
propertyKey: string | symbol,
parameterIndex: number
) => void;

通过 TypeScript 的声明文件,可以清晰的看到装饰器类型定义,对于不同类型的装饰器,其方法参数总是固定的且通过上下文注入。换言之,装饰器本身是不支持的自定义参数的,为了突破这一限制,通常使用工厂模式,即通过工厂函数接收参数,并返回一个装饰器函数。

装饰器工厂

上文提到,工厂模式并不是装饰器的过度设计,而是对其本身功能的再次加强,是为了提供可自定义入参。下面是一个方法装饰器工厂的一般形式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 方法装饰器工厂
function methodDecoratorFactory(msg: string) {
// do something...(预处理)
return funtion(target: Object,
propertyKey: string,
decorator: TypedPropertyDescriptor<any>) {
// 记录原方法
const fn = decorator.value;
// 重新定义方法,使用装饰器环绕原方法
// 不能使用箭头函数,否则会丢失上下文(this 会指向箭头函数)
decorator.value = function() {
// 前置处理
// arguments 需要解构传入,否则会将原参数包装成数组
// 或 使用 fn.apply(this. arguments)
const result = fn.call(this, ...arguments)
// 后置处理
// 必须返回原方法结果,否则会丢失结果
return result;
}
return decorator;
}
}

实践意义

日志装饰器

记录方法的入参、出参以及执行时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
export function log(hander?: Function) {
const loggerInfo = Object.seal({
method: "",
input: "",
output: "",
custom: "",
timeuse: "",
});
// 一个用于记录使用时间的对象
const timeUse = TimeUse.get();
return function (
target: Object,
key: string,
descriptor: TypedPropertyDescriptor<any>
): TypedPropertyDescriptor<any> {
const oldValue = descriptor.value;
descriptor.value = async function () {
loggerInfo.method = key;
const args: Array<any> = [];
for (let index in arguments) {
args.push(arguments[index]);
}
loggerInfo.input = args.join(",");
// 执行原方法
const value = await oldValue.apply(this, arguments);
loggerInfo.output = value;
hander &&
(loggerInfo.custom = hander(loggerInfo.input, loggerInfo.output) || "");

// 被调用时,会自动发出一个事件
loggerInfo.timeuse = `${timeUse.off()}ms`;
console.debug(loggerInfo);
return value;
};
return descriptor;
};
}

加载装饰器

在异步请求数据时显示加载动画。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export const loading = function (message: string) {
return function (
target: Object,
propertyKey: string,
decorator: TypedPropertyDescriptor<any>
) {
// 记录原方法
const fn = decorator.value;
decorator.value = async function () {
try {
// Loading 是 UI 框架中的加载动画
Loading.show({ message });
const resp = await fn.apply(this, arguments);
return resp;
} finally {
// 此时需要在 finally 关闭动画,防止异常时被阻塞
Loading.hide();
}
};
return decorator;
};
};

注意事项

装饰器只能在类及其成员属性上使用,不能作用在静态方法(类方法)上。

评论