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