TypeScript Stage 3字段装饰器改造:监听字段变更失效问题排查
问题解析与正确实现
关于context.access.set的obj参数
obj参数是当前被修改属性的类实例引用。Stage 3装饰器基于类的元编程模型,作用于类的字段定义而非单个实例,因此setter需要明确知道是哪个实例的属性在被修改,obj就是该实例的指向。
你的实现存在的问题
- setter签名不匹配:
context.access.set要求的签名是(obj: T, value: V) => void,你的代码缺少obj参数,导致类型错误和逻辑失效。 - 无限递归风险:直接在setter中执行
this[context.name] = newVal会再次触发setter,陷入死循环。 - 实例状态共享:你用初始化时的
value变量存储属性值,所有实例会共享同一个值,导致状态混乱,无法独立维护每个实例的属性状态。
正确的Stage 3字段装饰器实现
方案一:基于实例私有存储(推荐)
这种方式为每个实例独立存储属性值,避免状态共享,逻辑清晰:
function watchChange<T, V>(target: any, context: ClassFieldDecoratorContext<T, V>) { // 生成唯一私有符号,用于存储每个实例的属性值,避免命名冲突 const storageKey = Symbol(`${String(context.name)}_internal`); return function (initialValue: V) { // 初始化当前实例的私有存储 this[storageKey] = initialValue; // 为当前实例定义带监听逻辑的属性访问器 Object.defineProperty(this, context.name, { get() { return this[storageKey]; }, set(newVal: V) { console.log(`${String(context.name)} changed from ${this[storageKey]} to ${newVal}`); this[storageKey] = newVal; }, configurable: true, enumerable: true, }); // 返回undefined,让原字段被我们定义的访问器覆盖 return undefined; }; }
方案二:基于context.access扩展原访问器
如果需要保留原字段的访问器逻辑,可以通过包裹原setter实现:
function watchChange<T, V>(target: any, context: ClassFieldDecoratorContext<T, V>) { const originalSet = context.access.set; const storageKey = Symbol(`${String(context.name)}_internal`); return function (initialValue: V) { this[storageKey] = initialValue; // 自定义setter,接收实例和新值 const customSet = (obj: T, newVal: V) => { console.log(`${String(context.name)} changed from ${obj[storageKey]} to ${newVal}`); obj[storageKey] = newVal; // 如果存在原setter,调用原逻辑 if (originalSet) originalSet(obj, newVal); }; // 自定义getter const customGet = (obj: T) => { const val = obj[storageKey]; // 如果存在原getter,调用原逻辑 return context.access.get ? context.access.get(obj) : val; }; // 替换原访问器 context.access.set = customSet; context.access.get = customGet; return initialValue; }; }
使用示例
class Test { @watchChange public name = "initial"; } const test = new Test(); test.name = "updated"; // 输出:name changed from initial to updated console.log(test.name); // 输出:updated
内容的提问来源于stack exchange,提问作者Barosanu240
相关产品推荐
相关产品推荐

