Skip to content

@Computed 装饰器只适用于 getter 属性

背景

@Computed() 装饰器的核心作用是将 class 中的 getter 方法转换为 Vue 的 computed 响应式计算属性,从而获得缓存和依赖追踪能力。本文分析了 4 种属性场景,说明为什么 @Computed 只适合用于纯 getter 属性。

场景分析

场景 1:只有 getter 属性 ✅ 适合

typescript
class MyService {
  count = 0;

  @Computed()
  get doubleCount() {
    return this.count * 2;
  }
}

这是 @Computed 的标准使用场景。getter 本身是一个纯粹的派生计算——它依赖其他响应式数据,返回一个计算结果。通过 @Computed 将其转换为 Vue computed,可以获得:

  • 依赖追踪:当 count 变化时,doubleCount 自动感知
  • 缓存能力:count 未变化时,多次访问 doubleCount 不会重复计算

场景 2:只有 setter 属性 ❌ 不适合

typescript
class MyService {
  firstName = '';
  lastName = '';

  set fullName(value: string) {
    const [first, last] = value.split(' ');
    this.firstName = first;
    this.lastName = last;
  }
}

setter 函数的职责是接收一个值,然后更新其他属性。它本身不产生返回值,也不存在"计算结果需要缓存"的需求。Vue 的 computed 核心价值在于缓存 getter 的计算结果,而 setter 只是一个赋值操作的入口,不需要经过 computed 的处理。

此外,从装饰器类型约束的角度来看,@Computed 的类型签名是 ClassGetterDecoratorContext,TypeScript 本身就不允许将其应用于 setter。

场景 3:同时有 getter 和 setter 属性 ❌ 不适合

typescript
class MyService {
  firstName = '';
  lastName = '';

  @Computed()
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }

  set fullName(value: string) {
    const [first, last] = value.split(' ');
    this.firstName = first;
    this.lastName = last;
  }
}

虽然 getter 部分确实有缓存需求,但 setter 部分的存在引入了不必要的复杂度:

  1. @Computed 需要通过原型链遍历查找对应的 setter,增加了实现复杂度
  2. setter 本身只是更新 firstNamelastName,这些属性在 DI 系统中已经是响应式的,赋值操作天然会触发响应式更新
  3. 将 setter 绑定到 computed 的 set 方法上并没有带来额外价值——它只是一个透传

实际上,如果需要这种模式,更好的做法是只对 getter 使用 @Computed,setter 保持为普通方法:

typescript
class MyService {
  firstName = '';
  lastName = '';

  @Computed()
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }

  setFullName(value: string) {
    const [first, last] = value.split(' ');
    this.firstName = first;
    this.lastName = last;
  }
}

场景 4:accessor 属性 ❌ 不适合

typescript
class MyService {
  accessor name = 'hello';
}

TC39 Stage 3 的 accessor 关键字会自动为属性生成 getter 和 setter,本质上它就是一个普通属性的语法糖。在 DI 系统中,服务实例通过 onActivation 钩子已经被 reactive() 包装为响应式对象,所有普通属性的读写天然具备响应式能力。

accessor 属性使用 @Computed 没有意义,因为:

  1. accessor 本质上是一个存储值的普通属性,不是派生计算
  2. 响应式系统已经自动追踪了它的变化
  3. 它不存在"需要缓存的计算逻辑"

关于 Vue computed 支持 get/set 的补充说明

Vue 的 computed 函数确实支持同时传入 get 和 set:

typescript
const fullName = computed({
  get: () => `${firstName.value} ${lastName.value}`,
  set: (value: string) => {
    const [first, last] = value.split(' ');
    firstName.value = first;
    lastName.value = last;
  },
});

但这并不意味着 @Computed 装饰器也需要支持 setter。原因在于:computed 的 set 并不会"计算"什么——它只是一个回调入口,让你在赋值时执行自定义逻辑。本质上它是个语法糖,让你可以写 obj.fullName = 'John Doe' 而不是 obj.setFullName('John Doe')

computed 的核心价值在于 get 端的缓存和依赖追踪,set 端只是一个"顺便"提供的便利功能。在 @Computed 装饰器的场景下,支持 set 没有带来实际收益,却增加了实现复杂度(需要遍历原型链查找 setter 并透传)。如果用户确实需要一个"赋值入口",用普通方法就够了。

结论

@Computed 的价值在于将派生计算转换为带缓存的响应式计算属性。只有纯 getter 属性才符合这个语义——它依赖其他数据,通过计算产生一个派生值,并且这个计算结果值得被缓存。

setter、getter+setter 组合、accessor 属性都不适合 @Computed,因为它们要么不涉及计算(setter、accessor),要么引入了不必要的复杂度(getter+setter 组合)。简化 @Computed 的实现,使其只关注纯 getter 场景,能够显著降低代码复杂度。