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```