我正在编写一个简短的装饰辅助函数来将类转换为事件监听器
我的问题是装饰器会将装饰方法注册为传入事件的回调,但装饰方法不会保留原始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
}
}
答案 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也可以替换为常规属性,尤其是符号。