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