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