【问题标题】:Referncing decorated class method with this context intact在此上下文完整的情况下引用装饰类方法
【发布时间】:2018-10-22 13:10:39
【问题描述】:

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

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

}

【问题讨论】:

    标签: typescript decorator


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

    【讨论】:

    • 感谢您的详细回复。尽管尝试了 bind-decorator 解决方案,但由于某种原因,descriptor.value 在调用 bind 后返回 undefined
    • 两者都不是,但我会尝试类似的,感谢您的时间:)
    • 我更新了答案。我希望它适用于您的情况。无论如何,如果您的情况没有生命周期挂钩,则订阅应在类构造函数中进行,即应扩展一个类。请注意,使用了 hasOwnMetadata,即层次结构中的每个类都有自己的subscriptions。由于在具有子上下文的子类中从父类调用callback 是不可取的(可以覆盖具有相同名称的方法,而此调用类似于 super[methodName]()),我们应该只通过以下方式调用它们方法名称。
    • 很高兴它有帮助。
    猜你喜欢
    • 2023-02-07
    • 2020-12-08
    • 2011-05-20
    • 1970-01-01
    • 2012-02-08
    • 2011-11-29
    • 1970-01-01
    • 1970-01-01
    • 2013-01-05
    相关资源
    最近更新 更多