Ссылка на метод декорированного класса с этим контекстом нетронутым - PullRequest
0 голосов
/ 12 мая 2018

Я пишу короткую вспомогательную функцию декоратора, чтобы превратить класс в прослушиватель событий

Моя проблема в том, что декораторы зарегистрируют декорированный метод в качестве обратного вызова для входящих событий, но декорированный метод выиграл 'сохранить его оригинальный 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 голосов
/ 12 мая 2018

Проблема в том, что у декоратора нет доступа к this экземпляру класса.Он определяется только один раз при определении класса, target является прототипом класса.Чтобы получить экземпляр класса, он должен украсить метод или конструктор класса (расширить класс) и получить из него this.

Это особый случай this проблема .jobCreated используется как обратный вызов, поэтому он должен быть привязан к контексту.Кратчайший способ сделать это - определить его как стрелку:

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

Однако это, скорее всего, не сработает из-за того, что Subscribe украшает прототип класса, а стрелки определены наэкземпляр класса.Для правильной обработки Subscribe должен дополнительно правильно обрабатывать свойства, как показано в в этом ответе .Есть некоторые проблемы проектирования , почему функции прототипа должны быть предпочтительнее стрелок, и это одна из них.

Декоратор может взять на себя ответственность за привязку метода к контексту.Поскольку метод экземпляра не существует в момент оценки декоратора, процесс подписки следует отложить до тех пор, пока он не будет.Если в классе доступны исправления для жизненного цикла, которые можно пропатчить, класс должен быть расширен в ловушке жизненного цикла, чтобы дополнить конструктор функциями подписки:

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 метаданных также можно заменить обычными свойствами, в частности символами.

...