Angular装饰器——两种实现方式

时间:2022-06-01 16:39:58

装饰器主要作用

我们知道装饰器的两个主要作用

1、在运行时更改对象的功能而不影响对象的现有功能

2、将通用行为包装成简单,可复用的代码片段,减少模板代码的数量

装饰器的定义

我们首先看看定义:

Decorators are functions called on classes, class elements, or other JavaScript syntax forms during definition. Decorators have three primary capabilities: They can replace the value that is being decorated with a matching value that has the same semantics. They can associate metadata with the value that is being decorated. This metadata can then be read externally and used for metaprogramming and introspection. They can provide access to the value that is being decorated, via metadata.

首先,我们知道装饰器是一个函数,而且定义在类,类元素(方法或者属性),其他js语法形式上。装饰器有三个主要能力:

1、用具有相同语义的配置值取代修饰的值。(属于改变行为功能)

2、将元数据与被修饰的值相关联,然后可以从外部读取此元数据并用于元编程和自我检查。(属于改变行为功能)

3、通过元数据提供对修饰的值的访问。

 

装饰器的技术本质

装饰器在本质上是一个高阶函数,它接受一个函数作为参数,返回另一个函数作为返回值。

从上一节我们知道装饰器有四类:类装饰器,属性装饰器,方法装饰器和访问装饰器。

今天我们学着使用两种方式来实现装饰器。

业务场景

我有一个提醒的类,属性包括提醒消息,提醒类型,方法有将提醒消息打印出来。

基于函数实现装饰器

代码如下:

export class Notifications{

    type:string;

    message:string;

    constructor() {
        this.type = 'Success';
        this.message = 'I have a dream';
    }

    notifyUser = function(){
        console.log(`${this.type} notification: ${this.message}`);
    }
}

现在我们使用装饰增强此类的功能,提供一个延迟若干秒后打印消息的功能,注意,我不会修改原始类额。

增加一个高阶函数

export const delayMiliseconds = (fn: Function, delay:number = 0) => () => {
    setTimeout(() => fn(), delay);
    return 'notifyUser is called';
};

此时调用执行一下:

const notification = new Notifications();
notification.notifyUser();
const delayNotification = delayMiliseconds(notification.notifyUser,3000);
delayNotification();

我们发现的确会延迟3秒执行,但是会报一个错误:

Angular装饰器——两种实现方式

原因是返回的那个函数的this属性不在指向原始的对象,因此找不到type属性,我们修改一下

export class DelayedNotification {
    type: string;

    message:string;

    constructor(type) {
        this.type = type;
        this.message = 'I have a dream';
    }

    public notifyUser = delayMiliseconds(() => {
        console.log(`${this.type} notification: ${this.message} checkTime:`, new Date().getSeconds());
    }, 3000);
}

调用一下试试:

const notification = new Notifications();
notification.notifyUser();
const delayNotification = new DelayedNotification('Success');
delayNotification.notifyUser();

打印日志如下:

Angular装饰器——两种实现方式

嗯,现在起到了增强原始类的作用。有一个问题,这个类并不通用,因为我不能每次增强都要重新创建一个类吧。

因此,我做了一个变通:

export function functionBasedNotificationFactory() {
	const type = 'Success';

	 function notifyUser() {
		console.log(`${type} notification`);
	}

	return {
		name: 'Success',
		notifyUser: delayMiliseconds(notifyUser, 300)
	}
}

我对基础类做一个扩展,,然后调用:

functionBasedNotificationFactory().notifyUser();

可以看出,我也起到了同样的效果,这次不用再增加一个类啦,其实这里的本质是用返回的对象装饰了老对象的notifyUser方法。虽然起到了装饰的作用,似乎还是不那么通用。

 

基于类实现装饰器

首先我们定义装饰器工厂方法:

export function delayMiliseconds( milliseconds: number = 0 ) {
    return function (
      target: Object, 
      propertyKey: string | symbol,
      descriptor: PropertyDescriptor
    ) {
  
     const originalMethod = descriptor.value;
  
     descriptor.value = function (...args) {
            setTimeout(() => {
              originalMethod.call(this, ...args);
             }, milliseconds); 
          };
  
      return descriptor;
    };
}

可以看到,我们对装饰器的属性描述符进行了扩展,在延迟一定的时间后执行原始函数。

我们解释一下返回的函数的三个参数:

  • target -  either the constructor function of the class for a static member or the prototype of the class for an instance member. In our example, Notification.prototype would be a target.
  • propertyKey - the method name that is being decorated.
  • PropertyDescriptor - describes a property on an [Object]:

target对应一个类的构造器或者实例成员的原型,propertyKey对应装饰成员的名字,而PrppertyDescriptor对应描述一个成员的属性对象。

我们可以看看PropertyDescriptor的定义:


interface PropertyDescriptor {
    configurable?: boolean;
    enumerable?: boolean;
    value?: any;
    writable?: boolean;
    get? (): any;
    set? (v: any): void;
}

我们的代码对value进行了扩展。

使用我们的装饰器:

export class Notifications{

    type:string;

    message:string;

    constructor() {
        this.type = 'Success';
        this.message = 'I have a dream';
    }

    @delayMiliseconds(3000)
    notifyUser() : void {
        console.log(`${this.type} notification: ${this.message} checkTime:`,new Date().getSeconds());
    }
}

//调用
const notification = new Notifications();
notification.notifyUser();

 

Angular装饰器——两种实现方式

所以使用基于类的方式来实现装饰器会有更好的扩展性。下一节我们尝试来实现一个复杂的装饰器。