最近有空学习了一下angular的基础知识,对于angular的装饰器有了一些理解。其实装饰器并非angular特有的,它是Typescript的语言特性。首先我们看看
什么是装饰器
我们先看看Typescript官方的说明:
装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。 装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。
这里我们举一个例子:
@Input() name:string = '装饰器';
这里的Input就是装饰器,我们定义了一个变量name,类型为string,初始值为'装饰器',如果没有Input的话,这段代码只能给出这些信息,但是因为有了装饰器,所以,我们还知道了这个变量是angular组件的一个输入值,好的,Input装饰增强了这个变量的功能和作用,起到了装饰的作用。理解装饰,只要明白哪些是基础功能,哪些是装饰功能,那么使用起来就没有问题啦。是不是理解啦,再多说一点,为啥会有装饰,本质上它其实就是设计模式中装饰者模式的实现。
装饰者模式:
指在不必
改变原类文件
或者使用继承
的情况下,动态扩展对象的功能
装饰器的分类
类装饰器:(target) => {}
方法装饰器:(target, methodName: string, descriptor: PropertyDescriptor) => {}
属性装饰器: (target, propertyName: string) => {}
参数装饰器: (target, methodName: string, paramIndex: number) => {}
为什么要使用装饰器?
其实为什么要使用装饰器,也不难理解,肯定是想增强某些功能呗,并且这些增强的功能还有一般性。
我们还是举个例子吧,这个例子是网上看到的,参考链接如下:
https://www.jianshu.com/p/56f2c08ec591
基本的业务情况是:
我们需要控制一个组件loading的状态,那么设isLoading为true或者false,来控制开关。
// Child Component
// html
<div *ngIf="isLoading">isLoading 为 true</div>
<div *ngIf="!isLoading">isLoading 为 false</div>
//ts
@Input() isLoading = false;
// Parent Component
<app-child [isLoading]="'false'"></app-child>
调用的时候,本意是传递一个false参数,实际上传递的是一个false字符串。所以最终显示的是
isLoading 为 true
coerceBooleanProperty:是@angular/cdk/coercion中的一个方法,主要用途将传入的值进行强制转化为boolean.
为了解决这个问题,我们需要对传递进来的值进行格式化
@Input()
get isLoading() {
return this._isLoading;
}
set isLoading(v) {
this._isLoading = coerceBooleanProperty(v);
}
_isLoading = false;
添加上述代码后,最终能正确显示啦
isLoading 为 false
问题解决了是好事情,但是我们也不能仅仅停留在这里,而应该更进一步思考,除了这个方案外,还有没有更好的方案,如果在其他地方也遇到类似情况,那么是不是都要写set和get方法呢?从代码的可维护性上来说,此方案能解决问题,但不是最好的方案,我们看看下面的方案。
如何使用装饰器?
我们在这里使用装饰器来解决前面遇到的问题,从问题可以看出,我们应该写一个属性装饰器:
function propDecoratorFactory<T, D>(name: string, fallback: (v: T) => D): (target: any, propName: string) => void {
function propDecorator(target: any, propName: string, originalDescriptor?: TypedPropertyDescriptor<any>): any {
// 创建私有名字
const privatePropName = `$$__${propName}`;
// 首先判断是否设置过该值,防止重复操作
if (Object.prototype.hasOwnProperty.call(target, privatePropName)) {
console.warn(`The prop "${privatePropName}" is already exist, it will be overrided by ${name} decorator.`);
}
// 利用Object.defineProperty创建监听
Object.defineProperty(target, privatePropName, {
configurable: true,
writable: true
});
// 触发情况:访问时会触发getter,设值时会触发setter
// 具体不解释,可以看es6对Object.defineProperty的解释
// 题外话,可以一同可看proxy,proxy对Object.defineProperty进行了优化。
return {
get(): string {
// get 雷同于Setter解释
return originalDescriptor && originalDescriptor.get
? originalDescriptor.get.bind(this)()
: this[privatePropName];
},
set(value: T): void {
// 首先判断修饰器下是否存在Setter,setter存在于originalDescriptor中,我们将先进行规定的format
// 然后将format值传入其中,调用set 方法继续按照开发者的要求执行
// 所以如下一个使用的执行顺序是:
// @Input @InputBoolean set value(v){ this._v = v }
// 1.调用Object.defineProperty对target上的$$__value进行监听,创建setter + getter
// 2.setter(内) 触发 ,对传入的值进行格式化(即:调用传入的fallback(v)),拿到格式化的值后format_v
// 3.调用originalDescriptor 中的setter(外:即开发者创建)方法,传入格式化的值format_v进行做后续处理
// 因此此处只会对传入的值进行格式化,不会做任何操作。
if (originalDescriptor && originalDescriptor.set) {
originalDescriptor.set.bind(this)(fallback(value));
}
this[privatePropName] = fallback(value);
}
};
}
return propDecorator;
}
定义装饰器入口和格式化函数
// 装饰器入口
export function InputBoolean(): any {
return propDecoratorFactory('InputBoolean', toBoolean);
}
// 格式化boolean函数
export function toBoolean(value: boolean | string): boolean {
return coerceBooleanProperty(value);
}
使用装饰器:
@Input() @InputBoolean() isLoading = false;
看到没,不用再写get和set方法,而且这个组件是可扩展的,可以用在任何boolean变量上。
我们可以分析一下这个函数propDecoratorFactory
这个函数的定义如下
// 因为我们需要一个通用的装饰器工厂函数,所以我们传入两个参数name和fallback
// name: 装饰器名字(如:InputBoolean)
// fallback:调用需要执行的函数(如:toBoolean)
// 其次因为装饰器分为5种,但是我们主要针对属性和方法装饰器,因此
// originalDescriptor可为空(属性装饰器不包括该值,可看前面装饰器的介绍)
function propDecoratorFactory(name:string,fallback:function){
return propDecorator(
target: any,
propName: string,
originalDescriptor?: TypedPropertyDescriptor<any>
){
...
}
}
此函数有两个参数,返回值是一个函数,而返回的函数又是由propDecorator
定义的。这个函数实际上是为装饰的属性增强了get和set方法。
至于为什么会增强,为什么使用了@InputBoolean()
后就增强了属性的方法,我们以后分析,现在知道怎么用就可以啦。