首页 > 解决方案 > 在此上下文完整的情况下引用装饰类方法

问题描述

我正在编写一个简短的装饰器辅助函数来将一个类变成一个事件监听器

我的问题是装饰器会将装饰方法注册为传入事件的回调,但装饰方法不会保留它的原始this上下文。

主要问题如何this在这种情况下保留装饰方法的上下文?

执行:

export function EventHandler (topicKey: any): ClassDecorator {
    return function (target: any) {
        const subscriptions = Reflect.getMetadata('subscriptions', target.prototype)

        const topic = Container.get<DomainTopicInterface>(topicKey)
        topic.subscribe(event => {
            if (subscriptions.length === 0) {
                throw new Error(`Event received for '${target.constructor.name}' but no handlers defined`)
            }
            subscriptions.forEach((subscription: any) => {
                subscription.callback(event) // <---- the this context is undefined
            })
        })

        return target
    }
}

export function Subscribe (targetClass: StaticDomainEvent<any>): MethodDecorator {
    return function (target: Function, methodName: string, descriptor: TypedPropertyDescriptor<any>) {
        let originalMethod = descriptor.value
        let subscriptions = Reflect.getMetadata('subscriptions', target)
        if (!subscriptions) { Reflect.defineMetadata('subscriptions', subscriptions = [], target) }

        subscriptions.push({
            methodName,
            targetClass,
            callback: originalMethod
        })
    }
}

示例用法:

@EventHandler(Infra.DOMAIN_TOPIC)
export class JobHandler {

    constructor (
        @Inject() private service: JobService
    ) {}

    @Subscribe(JobCreated)
    jobCreated (events: Observable<JobCreated>) {
        console.log(this) // undefined
    }

}

标签: typescriptdecorator

解决方案


问题是装饰器无法访问this类实例。它只在类定义上评估一次,target是类原型。为了获取类实例,它应该装饰类方法或构造函数(扩展一个类)并this从它内部获取。

这是这个问题的一个特例。jobCreated用作回调,因此它应该绑定到上下文。执行此操作的最短方法是将其定义为箭头:

@Subscribe(JobCreated)
jobCreated = (events: Observable<JobCreated>) => {
    console.log(this) // undefined
}

但是,这可能不起作用,因为Subscribe装饰类原型,而箭头是在类实例上定义的。为了正确处理此问题,Subscribe还应正确处理属性,如this answer所示。有一些设计问题,为什么应该优先使用原型函数而不是箭头,这就是其中之一。

装饰器可能负责将方法绑定到上下文。由于在评估装饰器时实例方法不存在,因此订阅过程应该推迟到它存在。除非类中有可以修补的生命周期钩子,否则应该在生命周期钩子中扩展一个类,以便为构造函数增加订阅功能:

export function EventHandler (topicKey: any): ClassDecorator {
    return function (target: any) {
        // run only once per class
        if (Reflect.hasOwnMetadata('subscriptions', target.prototype))
            return target;

        target = class extends (target as { new(...args): any; }) {
            constructor(...args) {
                super(...args);

                const topic = Container.get<DomainTopicInterface>(topicKey)
                topic.subscribe(event => {
                    if (subscriptions.length === 0) {
                        throw new Error(`Event received for '${target.constructor.name}'`)
                    }
                    subscriptions.forEach((subscription: any) => {
                        this[subscription.methodName](event); // this is available here
                    })
                })
            }
        } as any;


export function Subscribe (targetClass: StaticDomainEvent<any>): MethodDecorator {
    return function (target: any, methodName: string, descriptor: TypedPropertyDescriptor<any>) {
        // target is class prototype
        let subscriptions = Reflect.getOwnMetadata('subscriptions', target);

        subscriptions.push({
            methodName,
            targetClass
            // no `callback` because parent method implementation
            // doesn't matter in child classes
        })
    }
}

请注意,订阅发生在 之后super,这允许在需要时将原始类构造函数中的方法绑定到其他上下文。

Reflect元数据 API 也可以替换为常规属性,尤其是符号。


推荐阅读