Angular装饰器——Decorators

时间:2023-05-12 17:43:18

最近有空学习了一下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() 后就增强了属性的方法,我们以后分析,现在知道怎么用就可以啦。