TypeScript装饰器绑定addEventListener时this指向异常问题
错误原因
方法装饰器的执行时机是类定义阶段,此时拿到的target参数是类的原型对象,而非类的实例对象:
- 第一版实现中直接把方法绑定到
target.constructor(也就是Canvas类本身),实例属性width是挂载在实例上的,类本身不存在这个属性,所以回调执行时读取为undefined。 - 第二版实现中在装饰器执行时直接调用
target[key]触发getter,此时getter里的this指向的是类原型,绑定的是原型对象而非实例,且addEventListener仅在类定义阶段执行一次,后续生成的类实例对应的绑定方法根本没有被注册到事件监听器中,自然无法获取到实例属性。
修复方案
核心思路:将事件绑定逻辑延迟到类实例化阶段执行,绑定到当前生成的实例上,同时记录绑定的函数引用用于后续解绑,避免内存泄漏。
修复后的装饰器代码
// 依赖reflect-metadata用于存储元数据,也可以自定义属性挂载到原型上省略该依赖 import "reflect-metadata"; const LISTENER_META_KEY = Symbol("listener_config"); export function Listener(listenerLocation: string) { return (target: any, key: string, descriptor: PropertyDescriptor) => { // 先存储事件配置到类元数据,不直接执行绑定 const existListeners = Reflect.getMetadata(LISTENER_META_KEY, target) || []; existListeners.push({ methodName: key, eventConfig: listenerLocation, originalMethod: descriptor.value }); Reflect.defineMetadata(LISTENER_META_KEY, existListeners, target); }; } // 提供通用基类,继承该类的子类会自动绑定所有@Listener装饰的方法 export abstract class EventBindable { // 存储已绑定的事件信息,用于后续销毁解绑 #boundEvents: Array<{ target: EventTarget; event: string; handler: EventListener }> = []; constructor() { // 实例化时读取元数据,绑定事件到当前实例 const listeners = Reflect.getMetadata(LISTENER_META_KEY, Object.getPrototypeOf(this)) || []; for (const { methodName, eventConfig, originalMethod } of listeners) { const [targetName, eventName] = eventConfig.split(":"); let eventTarget: EventTarget; if (targetName === "window") eventTarget = window; else if (targetName === "document") eventTarget = document; else continue; // 绑定当前实例 const boundHandler = originalMethod.bind(this) as EventListener; eventTarget.addEventListener(eventName, boundHandler); this.#boundEvents.push({ target: eventTarget, event: eventName, handler: boundHandler }); // 把绑定后的方法挂载到实例,和原方法调用逻辑一致 Object.defineProperty(this, methodName, { value: boundHandler, writable: true, configurable: true }); } } // 实例销毁时主动调用该方法,解绑所有事件避免内存泄漏 destroy() { for (const { target, event, handler } of this.#boundEvents) { target.removeEventListener(event, handler); } this.#boundEvents = []; } }
调整后的Canvas类代码
export class Canvas extends EventBindable { constructor( public readonly width: number, public readonly height: number, public readonly canvas: HTMLCanvasElement | string ) { // 先执行父类构造完成事件绑定 super(); console.log(this.width); // 其余初始化逻辑 } @Listener('window:resize') windowResize() { console.log('w', this.width); } }
内容的提问来源于stack exchange,提问作者Get Off My Lawn
相关产品推荐
相关产品推荐

