使用此上下文完整引用修饰的类方法

时间:2018-05-12 07:16:59

标签: typescript decorator

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

我的问题是装饰器会将装饰方法注册为传入事件的回调,但装饰方法不会保留原始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
    }

}

1 个答案:

答案 0 :(得分:1)

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

这是this problem的一个特例。 jobCreated用作回调,因此应绑定到上下文。最简单的方法是将其定义为箭头:

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

然而,由于Subscribe装饰类原型,而箭头是在类实例上定义的,因此这可能不会起作用。为了正确处理此问题,Subscribe应该另外正确处理属性,如this answer中所示。有some design concerns为什么原型函数应优先于箭头,这就是其中之一。

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

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也可以替换为常规属性,尤其是符号。