1# \@Computed装饰器:计算属性
2
3\@Computed装饰器:计算属性,在被计算的值变化的时候,只会计算一次。主要应用于解决UI多次重用该属性从而重复计算导致的性能问题。
4
5
6状态变量的变化可以触发其关联\@Computed的重新计算。在阅读本文档前,建议提前阅读:[\@ComponentV2](./arkts-new-componentV2.md),[\@ObservedV2和\@Trace](./arkts-new-observedV2-and-trace.md),[\@Local](./arkts-new-local.md)。
7
8>**说明:**
9>
10>\@Computed装饰器从API version 12开始支持。
11>
12
13## 概述
14
15\@Computed为方法装饰器,装饰getter方法。\@Computed会检测被计算的属性变化,当被计算的属性变化时,\@Computed只会被求解一次。
16对于复杂的计算,\@Computed会有性能收益。
17
18
19## 装饰器说明
20\@Computed语法:
21
22```ts
23@Computed
24get varName(): T {
25    return value;
26}
27```
28
29| \@Computed方法装饰器 | 说明                                                  |
30| ------------------ | ----------------------------------------------------- |
31| 支持类型           | getter访问器。 |
32| 从父组件初始化      | 禁止。 |
33| 可初始化子组件      | \@Param  |
34| 被执行的时机        | \@ComponentV2被初始化时,计算属性会被触发计算。当被计算的值改变的时候,计算属性也会发生计算。 |
35|是否允许赋值         | @Computed装饰的属性是只读的,不允许赋值,详情见[使用限制](#使用限制)。 |
36
37## 使用限制
38
39- \@Computed为方法装饰器,仅能装饰getter方法。
40
41  ```ts
42  @Computed
43  get fullName() { // 正确用法
44    return this.firstName + ' ' + this.lastName;
45  }
46  @Computed val: number = 0; // 错误用法,编译时报错
47  @Computed
48  func() { // 错误用法,编译时报错
49  }
50  ```
51- 在\@Computed装饰的getter方法中,不能改变参与计算的属性。
52
53  ```ts
54  @Computed
55  get fullName() {
56    this.lastName += 'a'; // 错误,不能改变参与计算的属性
57    return this.firstName + ' ' + this.lastName;
58  }
59  ```
60
61- \@Computed不能和双向绑定!!连用,\@Computed装饰的是getter访问器,不会被子组件同步,也不能被赋值。开发者自己实现的计算属性的setter不生效,且产生运行时报错。
62
63  ```ts
64  @ComponentV2
65  struct Child {
66    @Param double: number = 100;
67    @Event $double: (val: number) => void;
68
69    build() {
70      Button('ChildChange')
71        .onClick(() => {
72          this.$double(200);
73        })
74    }
75  }
76
77  @Entry
78  @ComponentV2
79  struct Index {
80    @Local count: number = 100;
81
82    @Computed
83    get double() {
84      return this.count * 2;
85    }
86
87    // @Computed装饰的属性是只读的,开发者自己实现的setter不生效,且产生运行时报错
88    set double(newValue : number) {
89      this.count = newValue / 2;
90    }
91
92    build() {
93      Scroll() {
94        Column({ space: 3 }) {
95          Text(`${this.count}`)
96          // 错误写法,@Computed装饰的属性方法是只读的,无法和双向绑定连用
97          Child({ double: this.double!! })
98        }
99      }
100    }
101  }
102  ```
103
104- \@Computed为状态管理V2提供的能力,只能在\@ComponentV2和\@ObservedV2中使用。
105- 多个\@Computed一起使用时,警惕循环求解。
106
107  ```ts
108  @Local a : number = 1;
109  @Computed
110  get b() {
111    return this.a + ' ' + this.c;  // 错误写法,存在循环b -> c -> b
112  }
113  @Computed
114  get c() {
115    return this.a + ' ' + this.b; // 错误写法,存在循环c -> b -> c
116  }
117  ```
118
119## 使用场景
120### 当被计算的属性变化时,\@Computed装饰的getter访问器只会被求解一次
1211. 在自定义组件中使用计算属性
122
123- 点击第一个Button改变lastName,触发\@Computed fullName重新计算。
124- `this.fullName`被绑定在两个Text组件上,观察`fullName`日志,可以发现,计算只发生了一次。
125- 对于前两个Text组件,`this.lastName + ' '+ this.firstName`这段逻辑被求解了两次。
126- 如果UI中有多处需要使用`this.lastName + ' '+ this.firstName`这段计算逻辑,可以使用计算属性,减少计算次数。
127- 点击第二个Button,age自增,UI无变化。因为age非状态变量,只有被观察到的变化才会触发\@Computed fullName重新计算。
128
129```ts
130@Entry
131@ComponentV2
132struct Index {
133  @Local firstName: string = 'Li';
134  @Local lastName: string = 'Hua';
135  age: number = 20; // 无法触发Computed
136
137  @Computed
138  get fullName() {
139    console.info("---------Computed----------");
140    return this.firstName + ' ' + this.lastName + this.age;
141  }
142
143  build() {
144    Column() {
145      Text(this.lastName + ' ' + this.firstName)
146      Text(this.lastName + ' ' + this.firstName)
147      Divider()
148      Text(this.fullName)
149      Text(this.fullName)
150      Button('changed lastName').onClick(() => {
151        this.lastName += 'a';
152      })
153
154      Button('changed age').onClick(() => {
155        this.age++;  // 无法触发Computed
156      })
157    }
158  }
159}
160```
161
162但是需要注意,计算属性本身是有性能开销的,实际应用开发中:
163- 如果是上面这种简单计算,可以不使用计算属性。
164- 如果在视图中只使用一次,也可以不使用计算属性,建议直接求解。
165
1662. 在\@ObservedV2装饰的类中使用计算属性
167- 点击Button改变lastName,触发\@Computed fullName重新计算,且只被计算一次。
168
169```ts
170@ObservedV2
171class Name {
172  @Trace firstName: string = 'Li';
173  @Trace lastName: string = 'Hua';
174
175  @Computed
176  get fullName() {
177    console.info('---------Computed----------');
178    return this.firstName + ' ' + this.lastName;
179  }
180}
181
182const name: Name = new Name();
183
184@Entry
185@ComponentV2
186struct Index {
187  name1: Name = name;
188
189  build() {
190    Column() {
191      Text(this.name1.fullName)
192      Text(this.name1.fullName)
193      Button('changed lastName').onClick(() => {
194        this.name1.lastName += 'a';
195      })
196    }
197  }
198}
199```
200
201### \@Computed装饰的属性可以被\@Monitor监听变化
202下面的例子展示了使用计算属性求解fahrenheit和kelvin。
203- 点击“-”,celsius-- -> fahrenheit -> kelvin --> kelvin改变触发onKelvinMonitor。
204- 点击“+”,celsius++ -> fahrenheit -> kelvin --> kelvin改变触发onKelvinMonitor。
205
206```ts
207@Entry
208@ComponentV2
209struct MyView {
210  @Local celsius: number = 20;
211
212  @Computed
213  get fahrenheit(): number {
214    return this.celsius * 9 / 5 + 32; // C -> F
215  }
216
217  @Computed
218  get kelvin(): number {
219    return (this.fahrenheit - 32) * 5 / 9 + 273.15; // F -> K
220  }
221
222  @Monitor("kelvin")
223  onKelvinMonitor(mon: IMonitor) {
224    console.log("kelvin changed from " + mon.value()?.before + " to " + mon.value()?.now);
225  }
226
227  build() {
228    Column({ space: 20 }) {
229      Row({ space: 20 }) {
230        Button('-')
231          .onClick(() => {
232            this.celsius--;
233          })
234
235        Text(`Celsius ${this.celsius.toFixed(1)}`).fontSize(50)
236
237        Button('+')
238          .onClick(() => {
239            this.celsius++;
240          })
241      }
242
243      Text(`Fahrenheit ${this.fahrenheit.toFixed(2)}`).fontSize(50)
244      Text(`Kelvin ${this.kelvin.toFixed(2)}`).fontSize(50)
245    }
246    .width('100%')
247  }
248}
249```
250### \@Computed装饰的属性可以初始化\@Param
251下面的例子展示了\@Computed初始\@Param。
252- 点击`Button('-')`和`Button('+')`改变商品数量,`quantity`是被\@Trace装饰的,其改变时可以被观察到的。
253- `quantity`的改变触发`total`和`qualifiesForDiscount`重新计算,计算商品总价和是否可以享有优惠。
254- `total`和`qualifiesForDiscount`的改变触发子组件`Child`对应Text组件刷新。
255
256```ts
257@ObservedV2
258class Article {
259  @Trace quantity: number = 0;
260  unitPrice: number = 0;
261
262  constructor(quantity: number, unitPrice: number) {
263    this.quantity = quantity;
264    this.unitPrice = unitPrice;
265  }
266}
267
268@Entry
269@ComponentV2
270struct Index {
271  @Local shoppingBasket: Article[] = [new Article(1, 20), new Article(5, 2)];
272
273  @Computed
274  get total(): number {
275    return this.shoppingBasket.reduce((acc: number, item: Article) => acc + (item.quantity * item.unitPrice), 0);
276  }
277
278  @Computed
279  get qualifiesForDiscount(): boolean {
280    return this.total >= 100;
281  }
282
283  build() {
284    Column() {
285      Text(`Shopping List: `).fontSize(30)
286      ForEach(this.shoppingBasket, (item: Article) => {
287        Row() {
288          Text(`unitPrice: ${item.unitPrice}`)
289          Button('-').onClick(() => {
290            if (item.quantity > 0) {
291              item.quantity--;
292            }
293          })
294          Text(`quantity: ${item.quantity}`)
295          Button('+').onClick(() => {
296            item.quantity++;
297          })
298        }
299
300        Divider()
301      })
302      Child({ total: this.total, qualifiesForDiscount: this.qualifiesForDiscount })
303    }.alignItems(HorizontalAlign.Start)
304  }
305}
306
307@ComponentV2
308struct Child {
309  @Param total: number = 0;
310  @Param qualifiesForDiscount: boolean = false;
311
312  build() {
313    Row() {
314      Text(`Total: ${this.total} `).fontSize(30)
315      Text(`Discount: ${this.qualifiesForDiscount} `).fontSize(30)
316    }
317  }
318}
319```