• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# \@Computed装饰器:计算属性
2<!--Kit: ArkUI-->
3<!--Subsystem: ArkUI-->
4<!--Owner: @liwenzhen3-->
5<!--Designer: @s10021109-->
6<!--Tester: @TerryTsao-->
7<!--Adviser: @zhang_yixin13-->
8
9当开发者使用相同的计算逻辑重复绑定在UI上时,为了防止重复计算,可以使用\@Computed计算属性。计算属性中的依赖的状态变量变化时,只会计算一次。这解决了UI多次重用该属性导致的重复计算和性能问题。如下面例子。
10
11```ts
12@Computed
13get sum() {
14  return this.count1 + this.count2 + this.count3;
15}
16Text(`${this.count1 + this.count2 + this.count3}`) // 计算this.count1 + this.count2 + this.count3
17Text(`${this.count1 + this.count2 + this.count3}`) // 重复计算this.count1 + this.count2 + this.count3
18Text(`${this.sum}`) // 读取@Computed sum的缓存值,节省上述重复计算
19Text(`${this.sum}`) // 读取@Computed sum的缓存值,节省上述重复计算
20```
21
22在阅读本文档前,建议提前阅读:[\@ComponentV2](./arkts-new-componentV2.md),[\@ObservedV2和\@Trace](./arkts-new-observedV2-and-trace.md),[\@Local](./arkts-new-local.md)。
23
24>**说明:**
25>
26> \@Computed装饰器从API version 12开始支持。
27>
28> 从API version 12开始,该装饰器支持在原子化服务中使用。
29
30## 概述
31
32@Computed为方法装饰器,装饰getter方法。@Computed会检测被计算的属性变化,当被计算的属性变化时,@Computed只会被求解一次。不推荐在@Computed中修改变量,错误的使用会导致数据无法被追踪或appfreeze等问题,详情见[使用限制](#使用限制)。
33
34但需要注意,对于简单计算,不建议使用计算属性,因为计算属性本身也有开销。对于复杂的计算,\@Computed能带来性能收益。
35
36## 装饰器说明
37\@Computed语法:
38
39```ts
40@Computed
41get varName(): T {
42    return value;
43}
44```
45
46| \@Computed方法装饰器 | 说明                                                  |
47| ------------------ | ----------------------------------------------------- |
48| 支持类型           | getter访问器。 |
49| 从父组件初始化     | 禁止。 |
50| 可初始化子组件     | \@Param。  |
51| 被执行的时机       | \@ComponentV2中的\@Computed会在自定义组件创建的时候初始化,触发\@Computed计算。</br>\@ObservedV2装饰的类中的\@Computed,会在\@ObservedV2装饰的类实例创建后,异步初始化,触发\@Computed计算。</br>在\@Computed中计算的状态变量被改变时,计算属性会重新计算。 |
52| 是否允许赋值       | @Computed装饰的属性是只读的,不允许赋值,详情见[使用限制](#使用限制)。 |
53
54## 使用限制
55
56- \@Computed为方法装饰器,仅能装饰getter方法。
57
58  ```ts
59  @Computed
60  get fullName() { // 正确用法
61    return this.firstName + ' ' + this.lastName;
62  }
63  @Computed val: number = 0; // 错误用法,编译时报错
64  @Computed
65  func() { // 错误用法,编译时报错
66  }
67  ```
68- \@Computed装饰的方法只有在初始化,或者其被计算的状态变量改变时,才会发生重新计算。不建议开发者在\@Computed装饰的getter方法中做除获取数据外其余的逻辑操作,如下面例子。
69
70```ts
71@Entry
72@ComponentV2
73struct Page {
74  @Local firstName: string = 'Hua';
75  @Local lastName: string = 'Li';
76  @Local showFullNameRequestCount: number = 0;
77  private fullNameRequestCount: number = 0;
78
79  @Computed
80  get fullName() {
81    console.info(`fullName`);
82    // 不推荐在@Computed的计算中做赋值逻辑,因为@Computed本质是一个getter访问器,用来节约重复计算
83    // 在这个例子中,fullNameRequestCount仅代表@Computed计算次数,不能代表fullName被访问的次数
84    this.fullNameRequestCount++;
85    return this.firstName + ' ' + this.lastName;
86  }
87
88  build() {
89    Column() {
90      Text(`${this.fullName}`) // 获取一次fullName
91      Text(`${this.fullName}`) // 获取一次fullName,累计获取两次fullName,但是fullName不会重新计算,读取缓存值
92
93      // 点击Button,获取fullNameRequestCount次数
94      Text(`count ${this.showFullNameRequestCount}`)
95      Button('get fullName').onClick(() => {
96        this.showFullNameRequestCount = this.fullNameRequestCount;
97      })
98    }
99  }
100}
101```
102
103- 在\@Computed装饰的getter方法中,不能改变参与计算的属性,以防止重复执行计算属性导致的appfreeze。
104 在下面例子中,计算`fullName1`时触发了`this.lastName`的改变,`this.lastName`的改变,触发`fullName2`的计算,在`fullName2`的计算中,改变了`this.firstName`,再次触发`fullName1`的重新计算,从而导致循环计算,最终引起appfreeze。
105
106```ts
107@Entry
108@ComponentV2
109struct Page {
110  @Local firstName: string = 'Hua';
111  @Local lastName: string = 'Li';
112
113  @Computed
114  get fullName1() {
115    console.info(`fullName1`);
116    this.lastName += 'a'; // 错误,不能改变参与计算的属性
117    return this.firstName + ' ' + this.lastName;
118  }
119
120  @Computed
121  get fullName2() {
122    console.info(`fullName2`);
123    this.firstName += 'a'; // 错误,不能改变参与计算的属性
124    return this.firstName + ' ' + this.lastName;
125  }
126
127  build() {
128    Column() {
129      Text(`${this.fullName1}`)
130      Text(`${this.fullName2}`)
131    }
132  }
133}
134```
135
136- \@Computed不能和双向绑定!!连用,\@Computed装饰的是getter访问器,不会被子组件同步,也不能被赋值。开发者自己实现的计算属性的setter不生效,且产生编译时报错。
137
138  ```ts
139  @ComponentV2
140  struct Child {
141    @Param double: number = 100;
142    @Event $double: (val: number) => void;
143
144    build() {
145      Button('ChildChange')
146        .onClick(() => {
147          this.$double(200);
148        })
149    }
150  }
151
152  @Entry
153  @ComponentV2
154  struct Index {
155    @Local count: number = 100;
156
157    @Computed
158    get double() {
159      return this.count * 2;
160    }
161
162    // @Computed装饰的属性是只读的,开发者自己实现的setter不生效,且产生编译时报错
163    set double(newValue : number) {
164      this.count = newValue / 2;
165    }
166
167    build() {
168      Scroll() {
169        Column({ space: 3 }) {
170          Text(`${this.count}`)
171          // 错误写法,@Computed装饰的属性是只读的,无法与双向绑定连用。
172          Child({ double: this.double!! })
173        }
174      }
175    }
176  }
177  ```
178
179- \@Computed为状态管理V2提供的能力,只能在\@ComponentV2和\@ObservedV2中使用。
180- 多个\@Computed一起使用时,警惕循环求解,以防止计算过程中的死循环。
181
182  ```ts
183  @Local a : number = 1;
184  @Computed
185  get b() {
186    return this.a + ' ' + this.c;  // 错误写法,存在循环b -> c -> b
187  }
188  @Computed
189  get c() {
190    return this.a + ' ' + this.b; // 错误写法,存在循环c -> b -> c
191  }
192  ```
193
194## 使用场景
195### 当被计算的属性变化时,\@Computed装饰的getter访问器只会被求解一次
1961. 在自定义组件中使用计算属性。
197
198- 点击第一个Button改变lastName,触发\@Computed fullName重新计算。
199- `this.fullName`被绑定在两个Text组件上,观察`fullName`日志,可以发现,计算只发生了一次。
200- 对于前两个Text组件,`this.lastName + ' '+ this.firstName`这段逻辑被求解了两次。
201- 如果UI中有多处需要使用`this.lastName + ' '+ this.firstName`这段计算逻辑,可以使用计算属性,减少计算次数。
202- 点击第二个Button,age自增,UI无变化。因为age非状态变量,只有被观察到的变化才会触发\@Computed fullName重新计算。
203
204```ts
205@Entry
206@ComponentV2
207struct Index {
208  @Local firstName: string = 'Li';
209  @Local lastName: string = 'Hua';
210  age: number = 20; // 无法触发Computed
211
212  @Computed
213  get fullName() {
214    console.info('---------Computed----------');
215    return this.firstName + ' ' + this.lastName + this.age;
216  }
217
218  build() {
219    Column() {
220      Text(this.lastName + ' ' + this.firstName)
221      Text(this.lastName + ' ' + this.firstName)
222      Divider()
223      Text(this.fullName)
224      Text(this.fullName)
225      Button('changed lastName').onClick(() => {
226        this.lastName += 'a';
227      })
228
229      Button('changed age').onClick(() => {
230        this.age++;  // 无法触发Computed
231      })
232    }
233  }
234}
235```
236
237计算属性本身会带来性能开销,在实际应用开发中需要注意:
238- 对于简单的计算逻辑,可以不使用计算属性。
239- 如果计算逻辑在视图中仅使用一次,则不使用计算属性,直接求解。
240
2412. 在\@ObservedV2装饰的类中使用计算属性。
242- 点击Button改变lastName,触发\@Computed fullName重新计算,且只被计算一次。
243
244```ts
245@ObservedV2
246class Name {
247  @Trace firstName: string = 'Hua';
248  @Trace lastName: string = 'Li';
249
250  @Computed
251  get fullName() {
252    console.info('---------Computed----------');
253    return this.firstName + ' ' + this.lastName;
254  }
255}
256
257const name: Name = new Name();
258
259@Entry
260@ComponentV2
261struct Index {
262  name1: Name = name;
263
264  build() {
265    Column() {
266      Text(this.name1.fullName)
267      Text(this.name1.fullName)
268      Button('changed lastName').onClick(() => {
269        this.name1.lastName += 'a';
270      })
271    }
272  }
273}
274```
275
276### \@Computed装饰的属性可以被\@Monitor监听变化
277如何使用计算属性求解fahrenheit和kelvin。示例如下:
278- 点击“-”,celsius-- -> fahrenheit -> kelvin --> kelvin变化时调用onKelvinMonitor。
279- 点击“+”,celsius++ -> fahrenheit -> kelvin --> kelvin变化时调用onKelvinMonitor。
280
281```ts
282@Entry
283@ComponentV2
284struct MyView {
285  @Local celsius: number = 20;
286
287  @Computed
288  get fahrenheit(): number {
289    return this.celsius * 9 / 5 + 32; // C -> F
290  }
291
292  @Computed
293  get kelvin(): number {
294    return (this.fahrenheit - 32) * 5 / 9 + 273.15; // F -> K
295  }
296
297  @Monitor('kelvin')
298  onKelvinMonitor(mon: IMonitor) {
299    console.log('kelvin changed from ' + mon.value()?.before + ' to ' + mon.value()?.now);
300  }
301
302  build() {
303    Column({ space: 20 }) {
304      Row({ space: 20 }) {
305        Button('-')
306          .onClick(() => {
307            this.celsius--;
308          })
309
310        Text(`Celsius ${this.celsius.toFixed(1)}`).fontSize(50)
311
312        Button('+')
313          .onClick(() => {
314            this.celsius++;
315          })
316      }
317
318      Text(`Fahrenheit ${this.fahrenheit.toFixed(2)}`).fontSize(50)
319      Text(`Kelvin ${this.kelvin.toFixed(2)}`).fontSize(50)
320    }
321    .width('100%')
322  }
323}
324```
325### \@Computed装饰的属性可以初始化\@Param
326下面的例子使用\@Computed初始化\@Param。
327- 点击`Button('-')`和`Button('+')`改变商品数量,`quantity`是被\@Trace装饰的,其改变时可以被观察到的。
328- `quantity`的改变会触发`total`和`qualifiesForDiscount`重新计算,计算商品总价和是否可以享有优惠。
329- `total`和`qualifiesForDiscount`的改变会触发子组件`Child`对应Text组件刷新。
330
331```ts
332@ObservedV2
333class Article {
334  @Trace quantity: number = 0;
335  unitPrice: number = 0;
336
337  constructor(quantity: number, unitPrice: number) {
338    this.quantity = quantity;
339    this.unitPrice = unitPrice;
340  }
341}
342
343@Entry
344@ComponentV2
345struct Index {
346  @Local shoppingBasket: Article[] = [new Article(1, 20), new Article(5, 2)];
347
348  @Computed
349  get total(): number {
350    return this.shoppingBasket.reduce((acc: number, item: Article) => acc + (item.quantity * item.unitPrice), 0);
351  }
352
353  @Computed
354  get qualifiesForDiscount(): boolean {
355    return this.total >= 100;
356  }
357
358  build() {
359    Column() {
360      Text(`Shopping List: `).fontSize(30)
361      ForEach(this.shoppingBasket, (item: Article) => {
362        Row() {
363          Text(`unitPrice: ${item.unitPrice}`)
364          Button('-').onClick(() => {
365            if (item.quantity > 0) {
366              item.quantity--;
367            }
368          })
369          Text(`quantity: ${item.quantity}`)
370          Button('+').onClick(() => {
371            item.quantity++;
372          })
373        }
374
375        Divider()
376      })
377      Child({ total: this.total, qualifiesForDiscount: this.qualifiesForDiscount })
378    }.alignItems(HorizontalAlign.Start)
379  }
380}
381
382@ComponentV2
383struct Child {
384  @Param total: number = 0;
385  @Param qualifiesForDiscount: boolean = false;
386
387  build() {
388    Row() {
389      Text(`Total: ${this.total} `).fontSize(30)
390      Text(`Discount: ${this.qualifiesForDiscount} `).fontSize(30)
391    }
392  }
393}
394```