1# 状态管理优秀实践 2<!--Kit: ArkUI--> 3<!--Subsystem: ArkUI--> 4<!--Owner: @jiyujia926--> 5<!--Designer: @s10021109--> 6<!--Tester: @TerryTsao--> 7<!--Adviser: @zhang_yixin13--> 8 9本章节旨在帮助开发者提高ArkUI应用质量,重点提高状态管理效率。章节中列举了常见的低效开发场景及解决方案,并通过对比推荐与非推荐用法,帮助开发者正确使用状态变量,实现高性能开发。 10 11## 使用@ObjectLink代替@Prop减少不必要的深拷贝 12 13在应用开发中,父组件常向子组件传值。如果子组件不需要修改该状态变量,子组件使用@Prop装饰器会增加组件创建时间并影响性能,此时建议改用@ObjectLink。 14 15【反例】 16 17```ts 18@Observed 19class MyClass { 20 public num: number = 0; 21 22 constructor(num: number) { 23 this.num = num; 24 } 25} 26 27@Component 28struct PropChild { 29 @Prop testClass: MyClass; // @Prop装饰状态变量会深拷贝。 30 31 build() { 32 Text(`PropChild testNum ${this.testClass.num}`) 33 } 34} 35 36@Entry 37@Component 38struct Parent { 39 @State testClass: MyClass[] = [new MyClass(1)]; 40 41 build() { 42 Column() { 43 Text(`Parent testNum ${this.testClass[0].num}`) 44 .onClick(() => { 45 this.testClass[0].num += 1; 46 }) 47 48 // PropChild没有改变@Prop testClass: MyClass的值,所以这时最优的选择是使用@ObjectLink。 49 PropChild({ testClass: this.testClass[0] }) 50 } 51 } 52} 53``` 54 55在以上示例中,PropChild组件没有改变\@Prop testClass: MyClass的值,因此使用\@ObjectLink更为合适。因为@Prop会深拷贝数据带来性能开销,所以\@ObjectLink是比\@Link和\@Prop更优的选择。 56 57【正例】 58 59```ts 60@Observed 61class MyClass { 62 public num: number = 0; 63 64 constructor(num: number) { 65 this.num = num; 66 } 67} 68 69@Component 70struct PropChild { 71 @ObjectLink testClass: MyClass; // @ObjectLink装饰状态变量不会深拷贝。 72 73 build() { 74 Text(`PropChild testNum ${this.testClass.num}`) 75 } 76} 77 78@Entry 79@Component 80struct Parent { 81 @State testClass: MyClass[] = [new MyClass(1)]; 82 83 build() { 84 Column() { 85 Text(`Parent testNum ${this.testClass[0].num}`) 86 .onClick(() => { 87 this.testClass[0].num += 1; 88 }) 89 90 // 当子组件不需要本地修改状态时,应优先使用@ObjectLink,因为@Prop会执行深拷贝并带来性能开销,此时@ObjectLink是比@Link和@Prop更优的选择。 91 PropChild({ testClass: this.testClass[0] }) 92 } 93 } 94} 95``` 96 97 98## 不使用状态变量强行更新非状态变量关联组件 99 100【反例】 101 102 103```ts 104@Entry 105@Component 106struct MyComponent { 107 @State needsUpdate: boolean = true; 108 realStateArr: Array<number> = [4, 1, 3, 2]; // 未使用状态变量装饰器。 109 realState: Color = Color.Yellow; 110 111 updateUIArr(param: Array<number>): Array<number> { 112 const triggerAGet = this.needsUpdate; 113 return param; 114 } 115 updateUI(param: Color): Color { 116 const triggerAGet = this.needsUpdate; 117 return param; 118 } 119 build() { 120 Column({ space: 20 }) { 121 ForEach(this.updateUIArr(this.realStateArr), 122 (item: number) => { 123 Text(`${item}`) 124 }) 125 Text("add item") 126 .onClick(() => { 127 // 改变realStateArr不会触发UI视图更新。 128 this.realStateArr.push(this.realStateArr[this.realStateArr.length-1] + 1); 129 130 // 触发UI视图更新。 131 this.needsUpdate = !this.needsUpdate; 132 }) 133 Text("chg color") 134 .onClick(() => { 135 // 改变realState不会触发UI视图更新。 136 this.realState = this.realState == Color.Yellow ? Color.Red : Color.Yellow; 137 138 // 触发UI视图更新。 139 this.needsUpdate = !this.needsUpdate; 140 }) 141 }.backgroundColor(this.updateUI(this.realState)) 142 .width(200).height(500) 143 } 144} 145``` 146 147上述示例存在以下问题: 148 149- 应用程序希望控制UI更新逻辑,然而ArkUI的UI更新是由框架检测状态变量的变化触发的。 150 151- this.needsUpdate是一个自定义的状态变量,仅应用于其绑定的UI组件。变量this.realStateArr和this.realState没有被装饰,它们的变化不会触发UI刷新。 152 153- 但是在该应用中,用户试图通过this.needsUpdate的更新来带动常规变量this.realStateArr和this.realState的更新,这种方法不合理且会导致更新性能下降。 154 155【正例】 156 157解决此问题,需用\@State装饰realStateArr和realState成员变量。解决后就不再需要变量needsUpdate。 158 159```ts 160@Entry 161@Component 162struct CompA { 163 @State realStateArr: Array<number> = [4, 1, 3, 2]; 164 @State realState: Color = Color.Yellow; 165 build() { 166 Column({ space: 20 }) { 167 ForEach(this.realStateArr, 168 (item: number) => { 169 Text(`${item}`) 170 }) 171 Text("add item") 172 .onClick(() => { 173 // 改变realStateArr触发UI视图更新。 174 this.realStateArr.push(this.realStateArr[this.realStateArr.length-1] + 1); 175 }) 176 Text("chg color") 177 .onClick(() => { 178 // 改变realState触发UI视图更新。 179 this.realState = this.realState == Color.Yellow ? Color.Red : Color.Yellow; 180 }) 181 }.backgroundColor(this.realState) 182 .width(200).height(500) 183 } 184} 185``` 186 187## 精准控制状态变量关联的组件数 188 189建议每个状态变量关联的组件数少于20个。精准控制状态变量关联的组件数量可减少不必要的组件刷新,提升刷新效率。有时开发者会将同一状态变量绑定于多个同级组件属性,状态变化时将导致这些组件同步更新,产生不必要的刷新,当组件复杂度较高时会显著影响整体性能。相反,将该状态变量绑定在这些组件的父组件上,可以减少需要刷新的组件数,提高性能。 190 191【反例】 192 193```ts 194@Observed 195class Translate { 196 translateX: number = 20; 197} 198@Component 199struct Title { 200 @ObjectLink translateObj: Translate; 201 build() { 202 Row() { 203 // 此处'app.media.icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。 204 Image($r('app.media.icon')) 205 .width(50) 206 .height(50) 207 .translate({ 208 x:this.translateObj.translateX // this.translateObj.translateX 绑定在Image和Text组件上。 209 }) 210 Text("Title") 211 .fontSize(20) 212 .translate({ 213 x: this.translateObj.translateX 214 }) 215 } 216 } 217} 218@Entry 219@Component 220struct Page { 221 @State translateObj: Translate = new Translate(); 222 223 build() { 224 Column() { 225 Title({ 226 translateObj: this.translateObj 227 }) 228 Stack() { 229 } 230 .backgroundColor("black") 231 .width(200) 232 .height(400) 233 .translate({ 234 x:this.translateObj.translateX //this.translateObj.translateX 绑定在Stack和Button组件上。 235 }) 236 Button("move") 237 .translate({ 238 x:this.translateObj.translateX 239 }) 240 .onClick(() => { 241 this.getUIContext().animateTo({ 242 duration: 50 243 },()=>{ 244 this.translateObj.translateX = (this.translateObj.translateX + 50) % 150 245 }) 246 }) 247 } 248 } 249} 250``` 251 252在上面的示例中,状态变量this.translateObj.translateX被用在多个同级的子组件下,当this.translateObj.translateX变化时,会导致所有关联它的组件一起刷新,但实际上由于这些组件的变化是相同的,因此可以将这个属性绑定到他们共同的父组件上,来实现减少组件的刷新数量。经过分析,所有子组件均位于Page组件的Column下,因此将所有子组件相同的translate属性统一到Column上,来实现精准控制状态变量关联的组件数。 253 254【正例】 255 256```ts 257@Observed 258class Translate { 259 translateX: number = 20; 260} 261@Component 262struct Title { 263 build() { 264 Row() { 265 // 此处'app.media.icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。 266 Image($r('app.media.icon')) 267 .width(50) 268 .height(50) 269 Text("Title") 270 .fontSize(20) 271 } 272 } 273} 274@Entry 275@Component 276struct Page1 { 277 @State translateObj: Translate = new Translate(); 278 279 build() { 280 Column() { 281 Title() 282 Stack() { 283 } 284 .backgroundColor("black") 285 .width(200) 286 .height(400) 287 Button("move") 288 .onClick(() => { 289 this.getUIContext().animateTo({ 290 duration: 50 291 },()=>{ 292 this.translateObj.translateX = (this.translateObj.translateX + 50) % 150 293 }) 294 }) 295 } 296 .translate({ // 子组件Stack和Button设置了同一个translate属性,可以统一到Column上设置。 297 x: this.translateObj.translateX 298 }) 299 } 300} 301``` 302 303## 合理控制对象类型状态变量关联的组件数量 304 305如果将一个复杂对象定义为状态变量,需要合理控制其关联的组件数。当对象中某个成员属性发生变化时,会导致该对象关联的所有组件刷新,即使这些组件并未直接使用该属性。为了避免这种“冗余刷新”对性能产生影响,建议合理拆分该复杂对象,控制对象关联的组件数量。具体可参考[精准控制组件的更新范围](https://gitcode.com/openharmony/docs/blob/master/zh-cn/application-dev/performance/precisely-control-render-scope.md)和[状态管理合理使用开发指导](properly-use-state-management-to-develope.md)两篇文章。 306 307## 查询状态变量关联的组件数 308 309在应用开发中,可以通过HiDumper查看状态变量关联的组件数,进行性能优化。具体可参考[状态变量组件定位工具实践](https://gitcode.com/openharmony/docs/blob/master/zh-cn/application-dev/performance/state_variable_dfx_pratice.md)。 310 311## 避免在for、while等循环逻辑中频繁读取状态变量 312 313在应用开发中,应避免在循环逻辑中频繁读取状态变量,而是应该放在循环外面读取。 314 315【反例】 316 317```ts 318import hilog from '@ohos.hilog'; 319 320@Entry 321@Component 322struct Index { 323 @State message: string = ''; 324 325 build() { 326 Column() { 327 Button('点击打印日志') 328 .onClick(() => { 329 for (let i = 0; i < 10; i++) { 330 hilog.info(0x0000, 'TAG', '%{public}s', this.message); 331 } 332 }) 333 .width('90%') 334 .backgroundColor(Color.Blue) 335 .fontColor(Color.White) 336 .margin({ 337 top: 10 338 }) 339 } 340 .justifyContent(FlexAlign.Start) 341 .alignItems(HorizontalAlign.Center) 342 .margin({ 343 top: 15 344 }) 345 } 346} 347``` 348 349【正例】 350 351```ts 352import hilog from '@ohos.hilog'; 353 354@Entry 355@Component 356struct Index { 357 @State message: string = ''; 358 359 build() { 360 Column() { 361 Button('点击打印日志') 362 .onClick(() => { 363 let logMessage: string = this.message; 364 for (let i = 0; i < 10; i++) { 365 hilog.info(0x0000, 'TAG', '%{public}s', logMessage); 366 } 367 }) 368 .width('90%') 369 .backgroundColor(Color.Blue) 370 .fontColor(Color.White) 371 .margin({ 372 top: 10 373 }) 374 } 375 .justifyContent(FlexAlign.Start) 376 .alignItems(HorizontalAlign.Center) 377 .margin({ 378 top: 15 379 }) 380 } 381} 382``` 383 384## 建议使用临时变量替换状态变量 385 386在应用开发中,应尽量减少对状态变量的直接赋值,通过临时变量完成数据计算操作。 387 388状态变量发生变化时,ArkUI会查询依赖该状态变量的组件并执行该组件的更新方法,完成组件渲染的行为。通过使用临时变量的计算代替直接操作状态变量,可以使ArkUI仅在最后一次状态变量变更时查询并渲染组件,减少不必要的操作,从而提高应用性能。状态变量行为可参考[@State装饰器:组件内状态](arkts-state.md)。 389 390【反例】 391 392```ts 393import { hiTraceMeter } from '@kit.PerformanceAnalysisKit'; 394 395@Entry 396@Component 397struct Index { 398 @State message: string = ''; 399 400 appendMsg(newMsg: string) { 401 // 性能打点 402 hiTraceMeter.startTrace('StateVariable', 1); 403 this.message += newMsg; 404 this.message += ';'; 405 this.message += '<br/>'; 406 hiTraceMeter.finishTrace('StateVariable', 1); 407 } 408 409 build() { 410 Column() { 411 Button('点击打印日志') 412 .onClick(() => { 413 this.appendMsg('操作状态变量'); 414 }) 415 .width('90%') 416 .backgroundColor(Color.Blue) 417 .fontColor(Color.White) 418 .margin({ 419 top: 10 420 }) 421 } 422 .justifyContent(FlexAlign.Start) 423 .alignItems(HorizontalAlign.Center) 424 .margin({ 425 top: 15 426 }) 427 } 428} 429``` 430 431直接操作状态变量,三次触发计算函数,运行耗时结果如下: 432 433 434 435【正例】 436 437```ts 438import { hiTraceMeter } from '@kit.PerformanceAnalysisKit'; 439 440@Entry 441@Component 442struct Index { 443 @State message: string = ''; 444 445 appendMsg(newMsg: string) { 446 // 性能打点。 447 hiTraceMeter.startTrace('TemporaryVariable', 2); 448 let message = this.message; 449 message += newMsg; 450 message += ';'; 451 message += '<br/>'; 452 this.message = message; 453 hiTraceMeter.finishTrace('TemporaryVariable', 2); 454 } 455 456 build() { 457 Column() { 458 Button('点击打印日志') 459 .onClick(() => { 460 this.appendMsg('操作临时变量'); 461 }) 462 .width('90%') 463 .backgroundColor(Color.Blue) 464 .fontColor(Color.White) 465 .margin({ 466 top: 10 467 }) 468 } 469 .justifyContent(FlexAlign.Start) 470 .alignItems(HorizontalAlign.Center) 471 .margin({ 472 top: 15 473 }) 474 } 475} 476``` 477 478使用临时变量取代状态变量的计算,三次触发计算函数,运行耗时结果如下: 479 480 481 482【总结】 483| **计算方式** | **耗时(局限不同设备和场景,数据仅供参考)** | **说明** | 484| ------ | ------- | ------------------------------------- | 485| 直接操作状态变量 | 1.01ms | 增加了ArkUI不必要的查询和渲染行为,导致性能劣化。 | 486| 使用临时变量计算 | 0.63ms | 减少了ArkUI不必要的行为,优化性能。 |