# @Computed装饰器：计算属性

当开发者使用相同的计算逻辑重复绑定在UI上时，为了防止重复计算，可以使用@Computed计算属性。计算属性中依赖的状态变量变化时，只会计算一次。这解决了UI多次重用该属性导致的重复计算和性能问题。如下面例子。

```
@Computed
get sum() {
  return this.count1 + this.count2 + this.count3;
}
```

```
Text(`${this.count1 + this.count2 + this.count3}`) // 计算this.count1 + this.count2 + this.count3
Text(`${this.count1 + this.count2 + this.count3}`) // 重复计算this.count1 + this.count2 + this.count3
Text(`${this.sum}`) // 读取@Computed sum的缓存值，节省上述重复计算
Text(`${this.sum}`) // 读取@Computed sum的缓存值，节省上述重复计算
```

在阅读本文档前，建议提前阅读：@ComponentV2，@ObservedV2和@Trace，@Local。

@Computed装饰器从API version 12开始支持。

从API version 12开始，该装饰器支持在元服务中使用。

从API version 23开始，该装饰器支持在ArkTS卡片中使用。

#### 概述

@Computed为方法装饰器，装饰getter方法。@Computed会检测被计算的属性变化，当被计算的属性变化时，@Computed只会被求解一次。不建议在@Computed中修改变量，错误的使用会导致数据无法被追踪或appfreeze等问题，详情见使用限制。

但需要注意，对于简单计算，不建议使用计算属性，因为计算属性本身也有开销。对于复杂的计算，@Computed能带来性能收益。

#### 装饰器说明

@Computed语法：

```
@Computed
get varName(): T {
    return value;
}
```

| @Computed方法装饰器 | 说明 |
| --- | --- |
| 支持类型 | getter访问器。 |
| 从父组件初始化 | 禁止。 |
| 可初始化子组件 | @Param。 |
| 被执行的时机 | @ComponentV2中的@Computed会在自定义组件创建的时候初始化，触发@Computed计算。 @ObservedV2装饰的类中的@Computed，会在@ObservedV2装饰的类实例创建后，异步初始化，触发@Computed计算。 在@Computed中计算的状态变量被改变时，计算属性会重新计算。 |
| 是否允许赋值 | @Computed装饰的属性是只读的，不允许赋值，详情见使用限制。 |

#### 使用限制

*   @Computed为方法装饰器，仅能装饰getter方法。
    
    ```
    @Computed
    get fullName() { // 正确用法
      return this.firstName + ' ' + this.lastName;
    }
    @Computed val: number = 0; // 错误用法，编译时报错
    @Computed
    func() { // 错误用法，编译时报错
    }
    ```
    
*   @Computed装饰的方法只有在初始化，或者其被计算的状态变量改变时，才会发生重新计算。不建议开发者在@Computed装饰的getter方法中做除获取数据外其余的逻辑操作，如下面例子。
    
    ```
    @Entry
    @ComponentV2
    struct Page {
      @Local firstName: string = 'Hua';
      @Local lastName: string = 'Li';
      @Local showFullNameRequestCount: number = 0;
      private fullNameRequestCount: number = 0;
    
      @Computed
      get fullName() {
        console.info('fullName');
        // 不建议在@Computed的计算中做赋值逻辑，因为@Computed本质是一个getter访问器，用来节约重复计算
        // 在这个例子中，fullNameRequestCount仅代表@Computed计算次数，不能代表fullName被访问的次数
        this.fullNameRequestCount++;
        return this.firstName + ' ' + this.lastName;
      }
    
      build() {
        Column() {
          Text(`${this.fullName}`) // 获取一次fullName
          Text(`${this.fullName}`) // 获取一次fullName，累计获取两次fullName，但是fullName不会重新计算，读取缓存值
    
          // 点击Button，获取fullNameRequestCount次数
          Text(`count ${this.showFullNameRequestCount}`)
          Button('get fullName').onClick(() => {
            this.showFullNameRequestCount = this.fullNameRequestCount;
          })
        }
      }
    }
    ```
    
*   在@Computed装饰的getter方法中，不能改变参与计算的属性，以防止重复执行计算属性导致的appfreeze。
    
    在下面例子中，计算fullName1时触发了this.lastName的改变，this.lastName的改变，触发fullName2的计算，在fullName2的计算中，改变了this.firstName，再次触发fullName1的重新计算，从而导致循环计算，最终引起appfreeze。
    
    ```
    @Entry
    @ComponentV2
    struct Page {
      @Local firstName: string = 'Hua';
      @Local lastName: string = 'Li';
    
      @Computed
      get fullName1() {
        console.info('fullName1');
        this.lastName += 'a'; // 错误，不能改变参与计算的属性
        return this.firstName + ' ' + this.lastName;
      }
    
      @Computed
      get fullName2() {
        console.info('fullName2');
        this.firstName += 'a'; // 错误，不能改变参与计算的属性
        return this.firstName + ' ' + this.lastName;
      }
    
      build() {
        Column() {
          Text(`${this.fullName1}`)
          Text(`${this.fullName2}`)
        }
      }
    }
    ```
    
*   @Computed不能和双向绑定!!连用，@Computed装饰的是getter访问器，不会被子组件同步，也不能被赋值。开发者自己实现的计算属性的setter不生效，且产生编译时报错。
    
    ```
    @ComponentV2
    struct Child {
      @Param double: number = 100;
      @Event $double: (val: number) => void;
    
      build() {
        Button('ChildChange')
          .onClick(() => {
            this.$double(200);
          })
      }
    }
    
    @Entry
    @ComponentV2
    struct Index {
      @Local count: number = 100;
    
      @Computed
      get double() {
        return this.count * 2;
      }
    
      // @Computed装饰的属性是只读的，开发者自己实现的setter不生效，编译时报错。
      set double(newValue : number) {
        this.count = newValue / 2;
      }
    
      build() {
        Scroll() {
          Column({ space: 3 }) {
            Text(`${this.count}`)
            // 错误写法，@Computed装饰的属性是只读的，无法与双向绑定连用，编译时报错。
            Child({ double: this.double!! })
          }
        }
      }
    }
    ```
    
*   @Computed为状态管理V2提供的能力，只能在@ComponentV2和@ObservedV2中使用。
    
*   多个@Computed一起使用时，警惕循环求解，以防止计算过程中的死循环。
    
    ```
    @Local a : number = 1;
    @Computed
    get b() {
      return this.a + ' ' + this.c;  // 错误写法，存在循环b -> c -> b
    }
    @Computed
    get c() {
      return this.a + ' ' + this.b; // 错误写法，存在循环c -> b -> c
    }
    ```
    

#### 使用场景

#### 当被计算的属性变化时，@Computed装饰的getter访问器只会被求解一次

1.  在自定义组件中使用计算属性。
    
    *   点击第一个Button改变lastName，触发@Computed fullName重新计算。
    *   this.fullName被绑定在两个Text组件上，观察fullName日志，可以发现，计算只发生了一次。
    *   对于前两个Text组件，this.lastName + ' '+ this.firstName这段逻辑被求解了两次。
    *   如果UI中有多处需要使用this.lastName + ' '+ this.firstName这段计算逻辑，可以使用计算属性，减少计算次数。
    *   点击第二个Button，age自增，UI无变化。因为age非状态变量，只有被观察到的变化才会触发@Computed fullName重新计算。
    
    ```
    import { hilog } from '@kit.PerformanceAnalysisKit';
    
    const TAG = '[Sample_Textcomponent]';
    const DOMAIN = 0xF811;
    const BUNDLE = 'Textcomponent_';
    
    @Entry
    @ComponentV2
    struct CustomComponentUse {
      @Local firstName: string = 'Li';
      @Local lastName: string = 'Hua';
      age: number = 20; // 无法触发Computed
    
      @Computed
      get fullName() {
        hilog.info(DOMAIN, TAG, BUNDLE + '---------Computed----------');
        return this.firstName + ' ' + this.lastName + this.age;
      }
    
      build() {
        Column() {
          Text(this.lastName + ' ' + this.firstName)
          Text(this.lastName + ' ' + this.firstName)
          Divider()
          Text(this.fullName)
          Text(this.fullName)
          Button('changed lastName')
            .onClick(() => {
              this.lastName += 'a';
            })
    
          Button('changed age')
            .onClick(() => {
              this.age++;  // 无法触发Computed
            })
        }
      }
    }
    ```
    
    计算属性本身会带来性能开销，在实际应用开发中需要注意：
    
    *   对于简单的计算逻辑，可以不使用计算属性。
    *   如果计算逻辑在视图中仅使用一次，则不使用计算属性，直接求解。
2.  在@ObservedV2装饰的类中使用计算属性。
    
    点击Button改变lastName，触发@Computed fullName重新计算，且只被计算一次。
    
    ```
    import { hilog } from '@kit.PerformanceAnalysisKit';
    
    const TAG = '[Sample_Textcomponent]';
    const DOMAIN = 0xF811;
    const BUNDLE = 'Textcomponent_';
    
    @ObservedV2
    class Name {
      @Trace public firstName: string = 'Hua';
      @Trace public lastName: string = 'Li';
    
      @Computed
      get fullName() {
        hilog.info(DOMAIN, TAG, BUNDLE + '---------Computed----------');
        return this.firstName + ' ' + this.lastName;
      }
    }
    
    const name: Name = new Name();
    
    @Entry
    @ComponentV2
    struct ObservedV2ClassUser {
      name1: Name = name;
    
      build() {
        Column() {
          Text(this.name1.fullName)
          Text(this.name1.fullName)
          // 点击Button改变lastName，触发fullName重新计算，且只被计算一次
          Button('changed lastName').onClick(() => {
            this.name1.lastName += 'a';
          })
        }
      }
    }
    ```
    

#### @Computed装饰的属性可以被@Monitor监听变化

如何使用计算属性求解fahrenheit和kelvin。示例如下：

*   点击“-”，celsius-- -> fahrenheit -> kelvin --> kelvin变化时调用onKelvinMonitor。
    
*   点击“+”，celsius++ -> fahrenheit -> kelvin --> kelvin变化时调用onKelvinMonitor。
    
    ```
    import { hilog } from '@kit.PerformanceAnalysisKit';
    
    const TAG = '[Sample_Textcomponent]';
    const DOMAIN = 0xF811;
    const BUNDLE = 'Textcomponent_';
    
    @Entry
    @ComponentV2
    struct ComputedPropertyResolution {
      @Local celsius: number = 20;
    
      @Computed
      get fahrenheit(): number {
        return this.celsius * 9 / 5 + 32; // C -> F
      }
    
      @Computed
      get kelvin(): number {
        return (this.fahrenheit - 32) * 5 / 9 + 273.15; // F -> K
      }
    
      @Monitor('kelvin')
      onKelvinMonitor(mon: IMonitor) {
        hilog.info(DOMAIN, TAG, BUNDLE + 'kelvin changed from' + mon.value()?.before + ' to ' + mon.value()?.now);
      }
    
      build() {
        Column({ space: 20 }) {
          Row({ space: 20 }) {
            Button('-')
              .onClick(() => {
                this.celsius--;
              })
    
            Text(`Celsius ${this.celsius.toFixed(1)}`).fontSize(40)
    
            Button('+')
              .onClick(() => {
                this.celsius++;
              })
          }
    
          Text(`Fahrenheit ${this.fahrenheit.toFixed(2)}`).fontSize(40)
          Text(`Kelvin ${this.kelvin.toFixed(2)}`).fontSize(40)
        }
        .width('100%')
      }
    }
    ```
    

#### @Computed装饰的属性可以初始化@Param

下面的例子使用@Computed初始化@Param。

*   点击Button('-')和Button('+')改变商品数量，quantity是被@Trace装饰的，其改变时可以被观察到的。
    
*   quantity的改变会触发total和qualifiesForDiscount重新计算，计算商品总价和是否可以享有优惠。
    
*   total和qualifiesForDiscount的改变会触发子组件Child对应Text组件刷新。
    
    ```
    @ObservedV2
    class Article {
      @Trace public quantity: number = 0;
      public unitPrice: number = 0;
    
      constructor(quantity: number, unitPrice: number) {
        this.quantity = quantity;
        this.unitPrice = unitPrice;
      }
    }
    
    @Entry
    @ComponentV2
    struct ComputingInitParam {
      @Local shoppingBasket: Article[] = [new Article(1, 20), new Article(5, 2)];
    
      @Computed
      get total(): number {
        return this.shoppingBasket.reduce((acc: number, item: Article) => acc + (item.quantity * item.unitPrice), 0);
      }
    
      @Computed
      get qualifiesForDiscount(): boolean {
        return this.total >= 100;
      }
    
      build() {
        Column() {
          Text(`Shopping List: `)
            .fontSize(30)
          ForEach(this.shoppingBasket, (item: Article) => {
            Row() {
              Text(`unitPrice: ${item.unitPrice}`)
              // 点击Button减少quantity，触发total和qualifiesForDiscount重新计算
              Button('-')
                .onClick(() => {
                  if (item.quantity > 0) {
                    item.quantity--;
                  }
                })
              Text(`quantity: ${item.quantity}`)
              // 点击Button增加quantity，触发total和qualifiesForDiscount重新计算
              Button('+')
                .onClick(() => {
                  item.quantity++;
                })
            }
    
            Divider()
          })
          Child({ total: this.total, qualifiesForDiscount: this.qualifiesForDiscount })
        }.alignItems(HorizontalAlign.Start)
      }
    }
    
    @ComponentV2
    struct Child {
      @Param total: number = 0;
      @Param qualifiesForDiscount: boolean = false;
    
      build() {
        Row() {
          Text(`Total: ${this.total} `)
            .fontSize(30)
          Text(`Discount: ${this.qualifiesForDiscount} `)
            .fontSize(30)
        }
      }
    }
    ```