typescript - Refererar till dekorerad klassmetod med detta-kontext intakt
Jag skriver en kort hjälpfunktion för en dekoratör för att göra en klass till en händelselyssnare
Mitt problem är att dekoratörerna kommer att registrera den dekorerade metoden som en callback för inkommande händelser, men den dekorerade metoden kommer inte att behålla sitt ursprungliga this-sammanhang.
Huvudfrågan är hur jag kan behålla this-kontexten för den dekorerade metoden i detta scenario?
Implementering :
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
})
}
}
Exempel på användning:
@EventHandler(Infra.DOMAIN_TOPIC)
export class JobHandler {
constructor (
@Inject() private service: JobService
) {}
@Subscribe(JobCreated)
jobCreated (events: Observable<JobCreated>) {
console.log(this) // undefined
}
}
Svar #1
Problemet är att dekoratören inte har tillgång till this klassinstans. Den utvärderas endast en gång vid klassdefinition, target är klassprototypen. För att få klassinstansen bör den dekorera klassmetod eller konstruktor (utöka en klass) och få this från insidan av den.
Detta är ett specialfall av detta problem. jobCreated används som en callback, så det bör binds till kontexten. Det kortaste sättet att göra detta skulle vara att definiera det som en pil:
@Subscribe(JobCreated)
jobCreated = (events: Observable<JobCreated>) => {
console.log(this) // undefined
}
Men detta kommer sannolikt inte att fungera, på grund av att Subscribe dekorerar klassens prototyp, medan pilar definieras på klassinstansen. För att hantera detta korrekt bör Subscribe dessutom hantera egenskaper på rätt sätt, som visas i detta svar. Det finns vissa designfrågor kring varför prototypfunktioner bör föredras framför pilar, och detta är en av dem.
En dekoratör kan ta ansvar för att binda en metod till kontexten. Eftersom instansmetoden inte existerar vid det ögonblick då dekoratören utvärderas, bör prenumerationsprocessen skjutas upp tills den kommer att göra det. Om det inte finns några livscykelhakar tillgängliga i en klass som kan patchas, bör en klass utökas i livscykelhaken för att förstärka konstruktorn med prenumerationsfunktionalitet:
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
})
}
}
Observera att prenumeration sker efter super, detta gör det möjligt att binda metoder i den ursprungliga klasskonstruktören till andra sammanhang vid behov.
Reflect-metadata-API:et kan också ersättas med vanliga egenskaper, särskilt symboler.
