1# 状态管理合理使用开发指导 2<!--Kit: ArkUI--> 3<!--Subsystem: ArkUI--> 4<!--Owner: @jiyujia926--> 5<!--Designer: @s10021109--> 6<!--Tester: @TerryTsao--> 7<!--Adviser: @zhang_yixin13--> 8 9由于对状态管理当前的特性并不了解,许多开发者在使用状态管理进行开发时会遇到UI不刷新、刷新性能差的情况。对此,本篇将从两个方向,对一共五个典型场景进行分析,同时提供相应的正例和反例,帮助开发者学习如何合理使用状态管理进行开发。 10 11## 合理使用属性 12 13### 将简单属性数组合并成对象数组 14 15在开发过程中,我们经常会需要设置多个组件的同一种属性,比如Text组件的内容、组件的宽度、高度等样式信息等。将这些属性保存在一个数组中,配合ForEach进行使用是一种简单且方便的方法。 16 17```typescript 18@Entry 19@Component 20struct Index { 21 @State items: string[] = []; 22 @State ids: string[] = []; 23 @State age: number[] = []; 24 @State gender: string[] = []; 25 26 aboutToAppear() { 27 this.items.push("Head"); 28 this.items.push("List"); 29 for (let i = 0; i < 20; i++) { 30 this.ids.push("id: " + Math.floor(Math.random() * 1000)); 31 this.age.push(Math.floor(Math.random() * 100 % 40)); 32 this.gender.push(Math.floor(Math.random() * 100) % 2 == 0 ? "Male" : "Female"); 33 } 34 } 35 36 isRenderText(index: number) : number { 37 console.info(`index ${index} is rendered`); 38 return 1; 39 } 40 41 build() { 42 Row() { 43 Column() { 44 ForEach(this.items, (item: string) => { 45 if (item == "Head") { 46 Text("Personal Info") 47 .fontSize(40) 48 } else if (item == "List") { 49 List() { 50 ForEach(this.ids, (id: string, index) => { 51 ListItem() { 52 Row() { 53 Text(id) 54 .fontSize(20) 55 .margin({ 56 left: 30, 57 right: 5 58 }) 59 Text("age: " + this.age[index as number]) 60 .fontSize(20) 61 .margin({ 62 left: 5, 63 right: 5 64 }) 65 .position({x: 100}) 66 .opacity(this.isRenderText(index)) 67 .onClick(() => { 68 this.age[index]++; 69 }) 70 Text("gender: " + this.gender[index as number]) 71 .margin({ 72 left: 5, 73 right: 5 74 }) 75 .position({x: 180}) 76 .fontSize(20) 77 } 78 } 79 .margin({ 80 top: 5, 81 bottom: 5 82 }) 83 }) 84 } 85 } 86 }) 87 } 88 } 89 } 90} 91``` 92 93上述代码运行效果如下。 94 95 96 97页面内通过ForEach显示了20条信息,当点击某一条信息中age的Text组件时,可以通过日志发现其他的19条信息中age的Text组件也进行了刷新(这体现在日志上,所有的age的Text组件都打出了日志),但实际上其他19条信息的age的数值并没有改变,也就是说其他19个Text组件并不需要刷新。 98 99这是因为当前状态管理的一个特性。假设存在一个被[@State](./arkts-state.md)修饰的number类型的数组Num[],其中有20个元素,值分别为0到19。这20个元素分别绑定了一个Text组件,当改变其中一个元素,例如第0号元素的值从0改成1,除了0号元素绑定的Text组件会刷新之外,其他的19个Text组件也会刷新,即使1到19号元素的值并没有改变。 100 101这个特性普遍的出现在简单类型数组的场景中,当数组中的元素够多时,会对UI的刷新性能有很大的负面影响。这种“不需要刷新的组件被刷新”的现象即是“冗余刷新”,当“冗余刷新”的节点过多时,UI的刷新效率会大幅度降低,因此需要减少“冗余刷新”,也就是做到**精准控制组件的更新范围**。 102 103为了减少由简单的属性相关的数组引起的“冗余刷新”,需要将属性数组转变为对象数组,配合自定义组件,实现精准控制更新范围。下面为修改后的代码。 104 105```typescript 106@Observed 107class InfoList extends Array<Info> { 108}; 109@Observed 110class Info { 111 ids: number; 112 age: number; 113 gender: string; 114 115 constructor() { 116 this.ids = Math.floor(Math.random() * 1000); 117 this.age = Math.floor(Math.random() * 100 % 40); 118 this.gender = Math.floor(Math.random() * 100) % 2 == 0 ? "Male" : "Female"; 119 } 120} 121@Component 122struct Information { 123 @ObjectLink info: Info; 124 @State index: number = 0; 125 isRenderText(index: number) : number { 126 console.info(`index ${index} is rendered`); 127 return 1; 128 } 129 130 build() { 131 Row() { 132 Text("id: " + this.info.ids) 133 .fontSize(20) 134 .margin({ 135 left: 30, 136 right: 5 137 }) 138 Text("age: " + this.info.age) 139 .fontSize(20) 140 .margin({ 141 left: 5, 142 right: 5 143 }) 144 .position({x: 100}) 145 .opacity(this.isRenderText(this.index)) 146 .onClick(() => { 147 this.info.age++; 148 }) 149 Text("gender: " + this.info.gender) 150 .margin({ 151 left: 5, 152 right: 5 153 }) 154 .position({x: 180}) 155 .fontSize(20) 156 } 157 } 158} 159@Entry 160@Component 161struct Page { 162 @State infoList: InfoList = new InfoList(); 163 @State items: string[] = []; 164 aboutToAppear() { 165 this.items.push("Head"); 166 this.items.push("List"); 167 for (let i = 0; i < 20; i++) { 168 this.infoList.push(new Info()); 169 } 170 } 171 172 build() { 173 Row() { 174 Column() { 175 ForEach(this.items, (item: string) => { 176 if (item == "Head") { 177 Text("Personal Info") 178 .fontSize(40) 179 } else if (item == "List") { 180 List() { 181 ForEach(this.infoList, (info: Info, index) => { 182 ListItem() { 183 Information({ 184 info: info, 185 index: index 186 }) 187 } 188 .margin({ 189 top: 5, 190 bottom: 5 191 }) 192 }) 193 } 194 } 195 }) 196 } 197 } 198 } 199} 200``` 201 202上述代码的运行效果如下。 203 204 205 206修改后的代码使用对象数组代替了原有的多个属性数组,能够避免数组的“冗余刷新”的情况。这是因为对于数组来说,对象内的变化是无法感知的,数组只能观测数组项层级的变化,例如新增数据项,修改数据项(普通数组是直接修改数据项的值,在对象数组的场景下是整个对象被重新赋值,改变某个数据项对象中的属性不会被观测到)、删除数据项等。这意味着当改变对象内的某个属性时,对于数组来说,对象是没有变化的,也就不会去刷新。在当前状态管理的观测能力中,除了数组嵌套对象的场景外,对象嵌套对象的场景也是无法观测到变化的,这一部分内容将在[将复杂对象拆分成多个小对象的集合](#将复杂大对象拆分成多个小对象的集合)中讲到。同时修改代码时使用了自定义组件与ForEach的结合,这一部分内容将在[在ForEach中使用自定义组件搭配对象数组](#在foreach中使用自定义组件搭配对象数组)讲到。 207 208### 将复杂大对象拆分成多个小对象的集合 209 210> **说明:** 211> 212> 从API version 11开始,推荐优先使用[@Track装饰器](arkts-track.md)解决该场景的问题。 213 214在开发过程中,我们有时会定义一个大的对象,其中包含了很多样式相关的属性,并且在父子组件间传递这个对象,将其中的属性绑定在组件上。 215 216```typescript 217@Observed 218class UiStyle { 219 translateX: number = 0; 220 translateY: number = 0; 221 scaleX: number = 0.3; 222 scaleY: number = 0.3; 223 width: number = 336; 224 height: number = 178; 225 posX: number = 10; 226 posY: number = 50; 227 alpha: number = 0.5; 228 borderRadius: number = 24; 229 imageWidth: number = 78; 230 imageHeight: number = 78; 231 translateImageX: number = 0; 232 translateImageY: number = 0; 233 fontSize: number = 20; 234} 235@Component 236struct SpecialImage { 237 @ObjectLink uiStyle: UiStyle; 238 private isRenderSpecialImage() : number { // 显示组件是否渲染的函数 239 console.info("SpecialImage is rendered"); 240 return 1; 241 } 242 build() { 243 Image($r('app.media.icon')) // 此处'app.media.icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。 244 .width(this.uiStyle.imageWidth) 245 .height(this.uiStyle.imageHeight) 246 .margin({ top: 20 }) 247 .translate({ 248 x: this.uiStyle.translateImageX, 249 y: this.uiStyle.translateImageY 250 }) 251 .opacity(this.isRenderSpecialImage()) // 如果Image重新渲染,该函数将被调用 252 } 253} 254@Component 255struct PageChild { 256 @ObjectLink uiStyle: UiStyle 257 // 下面的函数用于显示组件是否被渲染 258 private isRenderColumn() : number { 259 console.info("Column is rendered"); 260 return 1; 261 } 262 private isRenderStack() : number { 263 console.info("Stack is rendered"); 264 return 1; 265 } 266 private isRenderImage() : number { 267 console.info("Image is rendered"); 268 return 1; 269 } 270 private isRenderText() : number { 271 console.info("Text is rendered"); 272 return 1; 273 } 274 275 build() { 276 Column() { 277 SpecialImage({ 278 uiStyle: this.uiStyle 279 }) 280 Stack() { 281 Column() { 282 Image($r('app.media.icon')) // 此处'app.media.icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。 283 .opacity(this.uiStyle.alpha) 284 .scale({ 285 x: this.uiStyle.scaleX, 286 y: this.uiStyle.scaleY 287 }) 288 .padding(this.isRenderImage()) 289 .width(300) 290 .height(300) 291 } 292 .width('100%') 293 .position({ y: -80 }) 294 Stack() { 295 Text("Hello World") 296 .fontColor("#182431") 297 .fontWeight(FontWeight.Medium) 298 .fontSize(this.uiStyle.fontSize) 299 .opacity(this.isRenderText()) 300 .margin({ top: 12 }) 301 } 302 .opacity(this.isRenderStack()) 303 .position({ 304 x: this.uiStyle.posX, 305 y: this.uiStyle.posY 306 }) 307 .width('100%') 308 .height('100%') 309 } 310 .margin({ top: 50 }) 311 .borderRadius(this.uiStyle.borderRadius) 312 .opacity(this.isRenderStack()) 313 .backgroundColor("#FFFFFF") 314 .width(this.uiStyle.width) 315 .height(this.uiStyle.height) 316 .translate({ 317 x: this.uiStyle.translateX, 318 y: this.uiStyle.translateY 319 }) 320 Column() { 321 Button("Move") 322 .width(312) 323 .fontSize(20) 324 .backgroundColor("#FF007DFF") 325 .margin({ bottom: 10 }) 326 .onClick(() => { 327 this.getUIContext().animateTo({ 328 duration: 500 329 },() => { 330 this.uiStyle.translateY = (this.uiStyle.translateY + 180) % 250; 331 }) 332 }) 333 Button("Scale") 334 .borderRadius(20) 335 .backgroundColor("#FF007DFF") 336 .fontSize(20) 337 .width(312) 338 .onClick(() => { 339 this.uiStyle.scaleX = (this.uiStyle.scaleX + 0.6) % 0.8; 340 }) 341 } 342 .position({ 343 y:666 344 }) 345 .height('100%') 346 .width('100%') 347 348 } 349 .opacity(this.isRenderColumn()) 350 .width('100%') 351 .height('100%') 352 353 } 354} 355@Entry 356@Component 357struct Page { 358 @State uiStyle: UiStyle = new UiStyle(); 359 build() { 360 Stack() { 361 PageChild({ 362 uiStyle: this.uiStyle 363 }) 364 } 365 .backgroundColor("#F1F3F5") 366 } 367} 368``` 369 370上述代码的运行效果如下。 371 372 373 374优化前点击move按钮的脏节点更新耗时如下图: 375 376 377 378在上面的示例中,UiStyle定义了多个属性,并且这些属性分别被多个组件关联。当点击任意一个按钮更改其中的某些属性时,会导致所有这些关联uiStyle的组件进行刷新,虽然它们其实并不需要进行刷新(因为组件的属性都没有改变)。通过定义的一系列isRender函数,可以观察到这些组件的刷新。当点击“move”按钮进行平移动画时,由于translateY的值的多次改变,会导致每一次都存在“冗余刷新”的问题,这对应用的性能有着很大的负面影响。 379 380这是因为当前状态管理的一个刷新机制,假设定义了一个有20个属性的类,创建类的对象实例,将20个属性绑定到组件上,这时修改其中的某个属性,除了这个属性关联的组件会刷新之外,其他的19个属性关联的组件也都会刷新,即使这些属性本身并没有发生变化。 381 382这个机制会导致在使用一个复杂大对象与多个组件关联时,刷新性能的下降。对此,推荐将一个复杂大对象拆分成多个小对象的集合,在保留原有代码结构的基础上,减少“冗余刷新”,实现精准控制组件的更新范围。 383 384```typescript 385@Observed 386class NeedRenderImage { // 在同一组件中使用的属性可以划分为相同的类 387 public translateImageX: number = 0; 388 public translateImageY: number = 0; 389 public imageWidth:number = 78; 390 public imageHeight:number = 78; 391} 392@Observed 393class NeedRenderScale { // 在一起使用的属性可以划分为相同的类 394 public scaleX: number = 0.3; 395 public scaleY: number = 0.3; 396} 397@Observed 398class NeedRenderAlpha { // 在不同地方使用的属性可以划分为相同的类 399 public alpha: number = 0.5; 400} 401@Observed 402class NeedRenderSize { // 在一起使用的属性可以划分为相同的类 403 public width: number = 336; 404 public height: number = 178; 405} 406@Observed 407class NeedRenderPos { // 在一起使用的属性可以划分为相同的类 408 public posX: number = 10; 409 public posY: number = 50; 410} 411@Observed 412class NeedRenderBorderRadius { // 在不同地方使用的属性可以划分为相同的类 413 public borderRadius: number = 24; 414} 415@Observed 416class NeedRenderFontSize { // 在不同地方使用的属性可以划分为相同的类 417 public fontSize: number = 20; 418} 419@Observed 420class NeedRenderTranslate { // 在一起使用的属性可以划分为相同的类 421 public translateX: number = 0; 422 public translateY: number = 0; 423} 424@Observed 425class UiStyle { 426 // 使用NeedRenderxxx类 427 needRenderTranslate: NeedRenderTranslate = new NeedRenderTranslate(); 428 needRenderFontSize: NeedRenderFontSize = new NeedRenderFontSize(); 429 needRenderBorderRadius: NeedRenderBorderRadius = new NeedRenderBorderRadius(); 430 needRenderPos: NeedRenderPos = new NeedRenderPos(); 431 needRenderSize: NeedRenderSize = new NeedRenderSize(); 432 needRenderAlpha: NeedRenderAlpha = new NeedRenderAlpha(); 433 needRenderScale: NeedRenderScale = new NeedRenderScale(); 434 needRenderImage: NeedRenderImage = new NeedRenderImage(); 435} 436@Component 437struct SpecialImage { 438 @ObjectLink uiStyle : UiStyle; 439 @ObjectLink needRenderImage: NeedRenderImage // 从其父组件接收新类 440 private isRenderSpecialImage() : number { // 显示组件是否渲染的函数 441 console.info("SpecialImage is rendered"); 442 return 1; 443 } 444 build() { 445 Image($r('app.media.background')) // 此处'app.media.icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。 446 .width(this.needRenderImage.imageWidth) // 使用this.needRenderImage.xxx 447 .height(this.needRenderImage.imageHeight) 448 .margin({top:20}) 449 .translate({ 450 x: this.needRenderImage.translateImageX, 451 y: this.needRenderImage.translateImageY 452 }) 453 .opacity(this.isRenderSpecialImage()) // 如果Image重新渲染,该函数将被调用 454 } 455} 456@Component 457struct PageChild { 458 @ObjectLink uiStyle: UiStyle; 459 @ObjectLink needRenderTranslate: NeedRenderTranslate; // 从其父组件接收新定义的NeedRenderxxx类的实例 460 @ObjectLink needRenderFontSize: NeedRenderFontSize; 461 @ObjectLink needRenderBorderRadius: NeedRenderBorderRadius; 462 @ObjectLink needRenderPos: NeedRenderPos; 463 @ObjectLink needRenderSize: NeedRenderSize; 464 @ObjectLink needRenderAlpha: NeedRenderAlpha; 465 @ObjectLink needRenderScale: NeedRenderScale; 466 // 下面的函数用于显示组件是否被渲染 467 private isRenderColumn() : number { 468 console.info("Column is rendered"); 469 return 1; 470 } 471 private isRenderStack() : number { 472 console.info("Stack is rendered"); 473 return 1; 474 } 475 private isRenderImage() : number { 476 console.info("Image is rendered"); 477 return 1; 478 } 479 private isRenderText() : number { 480 console.info("Text is rendered"); 481 return 1; 482 } 483 484 build() { 485 Column() { 486 SpecialImage({ 487 uiStyle: this.uiStyle, 488 needRenderImage: this.uiStyle.needRenderImage // 传递给子组件 489 }) 490 Stack() { 491 Column() { 492 Image($r('app.media.background')) // 此处'app.media.icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。 493 .opacity(this.needRenderAlpha.alpha) 494 .scale({ 495 x: this.needRenderScale.scaleX, // 使用this.needRenderXxx.xxx 496 y: this.needRenderScale.scaleY 497 }) 498 .padding(this.isRenderImage()) 499 .width(300) 500 .height(300) 501 } 502 .width('100%') 503 .position({ y: -80 }) 504 505 Stack() { 506 Text("Hello World") 507 .fontColor("#182431") 508 .fontWeight(FontWeight.Medium) 509 .fontSize(this.needRenderFontSize.fontSize) 510 .opacity(this.isRenderText()) 511 .margin({ top: 12 }) 512 } 513 .opacity(this.isRenderStack()) 514 .position({ 515 x: this.needRenderPos.posX, 516 y: this.needRenderPos.posY 517 }) 518 .width('100%') 519 .height('100%') 520 } 521 .margin({ top: 50 }) 522 .borderRadius(this.needRenderBorderRadius.borderRadius) 523 .opacity(this.isRenderStack()) 524 .backgroundColor("#FFFFFF") 525 .width(this.needRenderSize.width) 526 .height(this.needRenderSize.height) 527 .translate({ 528 x: this.needRenderTranslate.translateX, 529 y: this.needRenderTranslate.translateY 530 }) 531 532 Column() { 533 Button("Move") 534 .width(312) 535 .fontSize(20) 536 .backgroundColor("#FF007DFF") 537 .margin({ bottom: 10 }) 538 .onClick(() => { 539 this.getUIContext().animateTo({ 540 duration: 500 541 }, () => { 542 this.needRenderTranslate.translateY = (this.needRenderTranslate.translateY + 180) % 250; 543 }) 544 }) 545 Button("Scale") 546 .borderRadius(20) 547 .backgroundColor("#FF007DFF") 548 .fontSize(20) 549 .width(312) 550 .margin({ bottom: 10 }) 551 .onClick(() => { 552 this.needRenderScale.scaleX = (this.needRenderScale.scaleX + 0.6) % 0.8; 553 }) 554 Button("Change Image") 555 .borderRadius(20) 556 .backgroundColor("#FF007DFF") 557 .fontSize(20) 558 .width(312) 559 .onClick(() => { // 在父组件中,仍使用 this.uiStyle.endRenderXxx.xxx 更改属性 560 this.uiStyle.needRenderImage.imageWidth = (this.uiStyle.needRenderImage.imageWidth + 30) % 160; 561 this.uiStyle.needRenderImage.imageHeight = (this.uiStyle.needRenderImage.imageHeight + 30) % 160; 562 }) 563 } 564 .position({ 565 y: 616 566 }) 567 .height('100%') 568 .width('100%') 569 } 570 .opacity(this.isRenderColumn()) 571 .width('100%') 572 .height('100%') 573 } 574} 575@Entry 576@Component 577struct Page { 578 @State uiStyle: UiStyle = new UiStyle(); 579 build() { 580 Stack() { 581 PageChild({ 582 uiStyle: this.uiStyle, 583 needRenderTranslate: this.uiStyle.needRenderTranslate, // 传递needRenderxxx类给子组件 584 needRenderFontSize: this.uiStyle.needRenderFontSize, 585 needRenderBorderRadius: this.uiStyle.needRenderBorderRadius, 586 needRenderPos: this.uiStyle.needRenderPos, 587 needRenderSize: this.uiStyle.needRenderSize, 588 needRenderAlpha: this.uiStyle.needRenderAlpha, 589 needRenderScale: this.uiStyle.needRenderScale 590 }) 591 } 592 .backgroundColor("#F1F3F5") 593 } 594} 595``` 596 597上述代码的运行效果如下。 598 599优化后点击move按钮的脏节点更新耗时如下图: 600 601 602 603修改后的代码将原来的大类中的十五个属性拆成了八个小类,并且在绑定的组件上也做了相应的适配。属性拆分遵循以下几点原则: 604 605- 只作用在同一个组件上的多个属性可以被拆分进同一个新类,即示例中的NeedRenderImage。适用于组件经常被不关联的属性改变而引起刷新的场景,这个时候就要考虑拆分属性,或者重新考虑ViewModel设计是否合理。 606- 经常被同时使用的属性可以被拆分进同一个新类,即示例中的NeedRenderScale、NeedRenderTranslate、NeedRenderPos、NeedRenderSize。适用于属性经常成对出现,或者被作用在同一个样式上的情况,例如.translate、.position、.scale等(这些样式通常会接收一个对象作为参数)。 607- 可能被用在多个组件上或相对较独立的属性应该被单独拆分进一个新类,即示例中的NeedRenderAlpha,NeedRenderBorderRadius、NeedRenderFontSize。适用于一个属性作用在多个组件上或者与其他属性没有联系的情况,例如.opacity、.borderRadius等(这些样式通常相对独立)。 608 609属性拆分的原理和属性合并类似,都是在嵌套场景下,状态管理无法观测二层以上的属性变化,所以不会因为二层的数据变化导致一层关联的其他属性被刷新,同时利用[@Observed](./arkts-observed-and-objectlink.md)和[@ObjectLink](./arkts-observed-and-objectlink.md)在父子节点间传递二层的对象,从而在子组件中正常的观测二层的数据变化,实现精准刷新。<!--Del-->关于属性拆分的详细内容,可以查看[精准控制组件的更新范围](../../performance/precisely-control-render-scope.md)。<!--DelEnd--> 610 611使用[@Track](./arkts-track.md)装饰器则无需做属性拆分,也能达到同样控制组件更新范围的作用。 612 613```ts 614@Observed 615class UiStyle { 616 @Track translateX: number = 0; 617 @Track translateY: number = 0; 618 @Track scaleX: number = 0.3; 619 @Track scaleY: number = 0.3; 620 @Track width: number = 336; 621 @Track height: number = 178; 622 @Track posX: number = 10; 623 @Track posY: number = 50; 624 @Track alpha: number = 0.5; 625 @Track borderRadius: number = 24; 626 @Track imageWidth: number = 78; 627 @Track imageHeight: number = 78; 628 @Track translateImageX: number = 0; 629 @Track translateImageY: number = 0; 630 @Track fontSize: number = 20; 631} 632@Component 633struct SpecialImage { 634 @ObjectLink uiStyle: UiStyle; 635 private isRenderSpecialImage() : number { // 显示组件是否渲染的函数 636 console.info("SpecialImage is rendered"); 637 return 1; 638 } 639 build() { 640 Image($r('app.media.foreground')) // 此处'app.media.icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。 641 .width(this.uiStyle.imageWidth) 642 .height(this.uiStyle.imageHeight) 643 .margin({ top: 20 }) 644 .translate({ 645 x: this.uiStyle.translateImageX, 646 y: this.uiStyle.translateImageY 647 }) 648 .opacity(this.isRenderSpecialImage()) // 如果Image重新渲染,该函数将被调用 649 } 650} 651@Component 652struct PageChild { 653 @ObjectLink uiStyle: UiStyle 654 // 下面的函数用于显示组件是否被渲染 655 private isRenderColumn() : number { 656 console.info("Column is rendered"); 657 return 1; 658 } 659 private isRenderStack() : number { 660 console.info("Stack is rendered"); 661 return 1; 662 } 663 private isRenderImage() : number { 664 console.info("Image is rendered"); 665 return 1; 666 } 667 private isRenderText() : number { 668 console.info("Text is rendered"); 669 return 1; 670 } 671 672 build() { 673 Column() { 674 SpecialImage({ 675 uiStyle: this.uiStyle 676 }) 677 Stack() { 678 Column() { 679 Image($r('app.media.foreground')) // 此处'app.media.icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。 680 .opacity(this.uiStyle.alpha) 681 .scale({ 682 x: this.uiStyle.scaleX, 683 y: this.uiStyle.scaleY 684 }) 685 .padding(this.isRenderImage()) 686 .width(300) 687 .height(300) 688 } 689 .width('100%') 690 .position({ y: -80 }) 691 Stack() { 692 Text("Hello World") 693 .fontColor("#182431") 694 .fontWeight(FontWeight.Medium) 695 .fontSize(this.uiStyle.fontSize) 696 .opacity(this.isRenderText()) 697 .margin({ top: 12 }) 698 } 699 .opacity(this.isRenderStack()) 700 .position({ 701 x: this.uiStyle.posX, 702 y: this.uiStyle.posY 703 }) 704 .width('100%') 705 .height('100%') 706 } 707 .margin({ top: 50 }) 708 .borderRadius(this.uiStyle.borderRadius) 709 .opacity(this.isRenderStack()) 710 .backgroundColor("#FFFFFF") 711 .width(this.uiStyle.width) 712 .height(this.uiStyle.height) 713 .translate({ 714 x: this.uiStyle.translateX, 715 y: this.uiStyle.translateY 716 }) 717 Column() { 718 Button("Move") 719 .width(312) 720 .fontSize(20) 721 .backgroundColor("#FF007DFF") 722 .margin({ bottom: 10 }) 723 .onClick(() => { 724 this.getUIContext().animateTo({ 725 duration: 500 726 },() => { 727 this.uiStyle.translateY = (this.uiStyle.translateY + 180) % 250; 728 }) 729 }) 730 Button("Scale") 731 .borderRadius(20) 732 .backgroundColor("#FF007DFF") 733 .fontSize(20) 734 .width(312) 735 .onClick(() => { 736 this.uiStyle.scaleX = (this.uiStyle.scaleX + 0.6) % 0.8; 737 }) 738 } 739 .position({ 740 y:666 741 }) 742 .height('100%') 743 .width('100%') 744 745 } 746 .opacity(this.isRenderColumn()) 747 .width('100%') 748 .height('100%') 749 750 } 751} 752@Entry 753@Component 754struct Page { 755 @State uiStyle: UiStyle = new UiStyle(); 756 build() { 757 Stack() { 758 PageChild({ 759 uiStyle: this.uiStyle 760 }) 761 } 762 .backgroundColor("#F1F3F5") 763 } 764} 765``` 766 767 768 769### 使用@Observed装饰或被声明为状态变量的类对象绑定组件 770 771在开发过程中,会有“重置数据”的场景,将一个新创建的对象赋值给原有的状态变量,实现数据的刷新。如果不注意新创建对象的类型,可能会出现UI不刷新的现象。 772 773```typescript 774@Observed 775class Child { 776 count: number; 777 constructor(count: number) { 778 this.count = count 779 } 780} 781@Observed 782class ChildList extends Array<Child> { 783}; 784@Observed 785class Ancestor { 786 childList: ChildList; 787 constructor(childList: ChildList) { 788 this.childList = childList; 789 } 790 public loadData() { 791 let tempList = [new Child(1), new Child(2), new Child(3), new Child(4), new Child(5)]; 792 this.childList = tempList; 793 } 794 795 public clearData() { 796 this.childList = [] 797 } 798} 799@Component 800struct CompChild { 801 @Link childList: ChildList; 802 @ObjectLink child: Child; 803 804 build() { 805 Row() { 806 Text(this.child.count+'') 807 .height(70) 808 .fontSize(20) 809 .borderRadius({ 810 topLeft: 6, 811 topRight: 6 812 }) 813 .margin({left: 50}) 814 Button('X') 815 .backgroundColor(Color.Red) 816 .onClick(()=>{ 817 let index = this.childList.findIndex((item) => { 818 return item.count === this.child.count 819 }) 820 if (index !== -1) { 821 this.childList.splice(index, 1); 822 } 823 }) 824 .margin({ 825 left: 200, 826 right:30 827 }) 828 } 829 .margin({ 830 top:15, 831 left: 15, 832 right:10, 833 bottom:15 834 }) 835 .borderRadius(6) 836 .backgroundColor(Color.Grey) 837 } 838} 839@Component 840struct CompList { 841 @ObjectLink@Watch('changeChildList') childList: ChildList; 842 843 changeChildList() { 844 console.info('CompList ChildList change'); 845 } 846 847 isRenderCompChild(index: number) : number { 848 console.info("Comp Child is render" + index); 849 return 1; 850 } 851 852 build() { 853 Column() { 854 List() { 855 ForEach(this.childList, (item: Child, index) => { 856 ListItem() { 857 CompChild({ 858 childList: this.childList, 859 child: item 860 }) 861 .opacity(this.isRenderCompChild(index)) 862 } 863 864 }) 865 } 866 .height('70%') 867 } 868 } 869} 870@Component 871struct CompAncestor { 872 @ObjectLink ancestor: Ancestor; 873 874 build() { 875 Column() { 876 CompList({ childList: this.ancestor.childList }) 877 Row() { 878 Button("Clear") 879 .onClick(() => { 880 this.ancestor.clearData() 881 }) 882 .width(100) 883 .margin({right: 50}) 884 Button("Recover") 885 .onClick(() => { 886 this.ancestor.loadData() 887 }) 888 .width(100) 889 } 890 } 891 } 892} 893@Entry 894@Component 895struct Page { 896 @State childList: ChildList = [new Child(1), new Child(2), new Child(3), new Child(4),new Child(5)]; 897 @State ancestor: Ancestor = new Ancestor(this.childList) 898 899 build() { 900 Column() { 901 CompAncestor({ ancestor: this.ancestor}) 902 } 903 } 904} 905``` 906 907上述代码运行效果如下。 908 909 910 911上述代码维护了一个ChildList类型的数据源,点击"X"按钮删除一些数据后再点击Recover进行恢复ChildList,发现再次点击"X"按钮进行删除时,UI并没有刷新,同时也没有打印出“CompList ChildList change”的日志。 912 913代码中对数据源childList重新赋值时,是通过Ancestor对象的方法loadData。 914 915```typescript 916 public loadData() { 917 let tempList = [new Child(1), new Child(2), new Child(3), new Child(4), new Child(5)]; 918 this.childList = tempList; 919 } 920``` 921 922在loadData方法中,创建了一个临时的Child类型的数组tempList,并且将Ancestor对象的成员变量的childList指向了tempList。但是这里创建的Child[]类型的数组tempList其实并没有能被观测的能力(也就说它的变化无法主动触发UI刷新)。当它被赋值给childList之后,触发了ForEach的刷新,使得界面完成了重建,但是再次点击删除时,由于此时的childList已经指向了新的tempList代表的数组,并且这个数组并没有被观测的能力,是个静态的量,所以它的更改不会被观测到,也就不会引起UI的刷新。实际上这个时候childList里的数据已经减少了,只是UI没有刷新。 923 924有些开发者会注意到,在Page中初始化定义childList的时候,也是以这样一种方法去进行初始化的。 925 926```typescript 927@State childList: ChildList = [new Child(1), new Child(2), new Child(3), new Child(4),new Child(5)]; 928@State ancestor: Ancestor = new Ancestor(this.childList) 929``` 930 931但是由于这里的childList实际上是被@State装饰了,根据当前状态管理的观测能力,尽管右边赋值的是一个Child[]类型的数据,它并没有被@Observed装饰,这里的childList却依然具备了被观测的能力,所以能够正常的触发UI的刷新。当去掉childList的@State的装饰器后,不去重置数据源,也无法通过点击“X”按钮触发刷新。 932 933因此,需要将具有观测能力的类对象绑定组件,来确保当改变这些类对象的内容时,UI能够正常的刷新。 934 935```typescript 936@Observed 937class Child { 938 count: number; 939 constructor(count: number) { 940 this.count = count 941 } 942} 943@Observed 944class ChildList extends Array<Child> { 945}; 946@Observed 947class Ancestor { 948 childList: ChildList; 949 constructor(childList: ChildList) { 950 this.childList = childList; 951 } 952 public loadData() { 953 let tempList = new ChildList(); 954 for (let i = 1; i < 6; i ++) { 955 tempList.push(new Child(i)); 956 } 957 this.childList = tempList; 958 } 959 960 public clearData() { 961 this.childList = [] 962 } 963} 964@Component 965struct CompChild { 966 @Link childList: ChildList; 967 @ObjectLink child: Child; 968 969 build() { 970 Row() { 971 Text(this.child.count+'') 972 .height(70) 973 .fontSize(20) 974 .borderRadius({ 975 topLeft: 6, 976 topRight: 6 977 }) 978 .margin({left: 50}) 979 Button('X') 980 .backgroundColor(Color.Red) 981 .onClick(()=>{ 982 let index = this.childList.findIndex((item) => { 983 return item.count === this.child.count 984 }) 985 if (index !== -1) { 986 this.childList.splice(index, 1); 987 } 988 }) 989 .margin({ 990 left: 200, 991 right:30 992 }) 993 } 994 .margin({ 995 top:15, 996 left: 15, 997 right:10, 998 bottom:15 999 }) 1000 .borderRadius(6) 1001 .backgroundColor(Color.Grey) 1002 } 1003} 1004@Component 1005struct CompList { 1006 @ObjectLink@Watch('changeChildList') childList: ChildList; 1007 1008 changeChildList() { 1009 console.info('CompList ChildList change'); 1010 } 1011 1012 isRenderCompChild(index: number) : number { 1013 console.info("Comp Child is render" + index); 1014 return 1; 1015 } 1016 1017 build() { 1018 Column() { 1019 List() { 1020 ForEach(this.childList, (item: Child, index) => { 1021 ListItem() { 1022 CompChild({ 1023 childList: this.childList, 1024 child: item 1025 }) 1026 .opacity(this.isRenderCompChild(index)) 1027 } 1028 1029 }) 1030 } 1031 .height('70%') 1032 } 1033 } 1034} 1035@Component 1036struct CompAncestor { 1037 @ObjectLink ancestor: Ancestor; 1038 1039 build() { 1040 Column() { 1041 CompList({ childList: this.ancestor.childList }) 1042 Row() { 1043 Button("Clear") 1044 .onClick(() => { 1045 this.ancestor.clearData() 1046 }) 1047 .width(100) 1048 .margin({right: 50}) 1049 Button("Recover") 1050 .onClick(() => { 1051 this.ancestor.loadData() 1052 }) 1053 .width(100) 1054 } 1055 } 1056 } 1057} 1058@Entry 1059@Component 1060struct Page { 1061 @State childList: ChildList = [new Child(1), new Child(2), new Child(3), new Child(4),new Child(5)]; 1062 @State ancestor: Ancestor = new Ancestor(this.childList) 1063 1064 build() { 1065 Column() { 1066 CompAncestor({ ancestor: this.ancestor}) 1067 } 1068 } 1069} 1070``` 1071 1072上述代码运行效果如下。 1073 1074 1075 1076核心的修改点是将原本Child[]类型的tempList修改为具有被观测能力的ChildList类。 1077 1078```typescript 1079public loadData() { 1080 let tempList = new ChildList(); 1081 for (let i = 1; i < 6; i ++) { 1082 tempList.push(new Child(i)); 1083 } 1084 this.childList = tempList; 1085 } 1086``` 1087 1088ChildList类型在定义的时候使用了@Observed进行装饰,所以用new创建的对象tempList具有被观测的能力,因此在点击“X”按钮删除其中一条内容时,变量childList就能够观测到变化,所以触发了ForEach的刷新,最终UI渲染刷新。 1089 1090## 合理使用ForEach/LazyForEach 1091 1092### 减少使用LazyForEach的重建机制刷新UI 1093 1094开发过程中通常会将[LazyForEach](arkts-rendering-control-lazyforeach.md)和状态变量结合起来使用。 1095 1096```typescript 1097class BasicDataSource implements IDataSource { 1098 private listeners: DataChangeListener[] = []; 1099 private originDataArray: StringData[] = []; 1100 1101 public totalCount(): number { 1102 return 0; 1103 } 1104 1105 public getData(index: number): StringData { 1106 return this.originDataArray[index]; 1107 } 1108 1109 registerDataChangeListener(listener: DataChangeListener): void { 1110 if (this.listeners.indexOf(listener) < 0) { 1111 console.info('add listener'); 1112 this.listeners.push(listener); 1113 } 1114 } 1115 1116 unregisterDataChangeListener(listener: DataChangeListener): void { 1117 const pos = this.listeners.indexOf(listener); 1118 if (pos >= 0) { 1119 console.info('remove listener'); 1120 this.listeners.splice(pos, 1); 1121 } 1122 } 1123 1124 notifyDataReload(): void { 1125 this.listeners.forEach(listener => { 1126 listener.onDataReloaded(); 1127 }) 1128 } 1129 1130 notifyDataAdd(index: number): void { 1131 this.listeners.forEach(listener => { 1132 listener.onDataAdd(index); 1133 }) 1134 } 1135 1136 notifyDataChange(index: number): void { 1137 this.listeners.forEach(listener => { 1138 listener.onDataChange(index); 1139 }) 1140 } 1141 1142 notifyDataDelete(index: number): void { 1143 this.listeners.forEach(listener => { 1144 listener.onDataDelete(index); 1145 }) 1146 } 1147 1148 notifyDataMove(from: number, to: number): void { 1149 this.listeners.forEach(listener => { 1150 listener.onDataMove(from, to); 1151 }) 1152 } 1153} 1154 1155class MyDataSource extends BasicDataSource { 1156 private dataArray: StringData[] = []; 1157 1158 public totalCount(): number { 1159 return this.dataArray.length; 1160 } 1161 1162 public getData(index: number): StringData { 1163 return this.dataArray[index]; 1164 } 1165 1166 public addData(index: number, data: StringData): void { 1167 this.dataArray.splice(index, 0, data); 1168 this.notifyDataAdd(index); 1169 } 1170 1171 public pushData(data: StringData): void { 1172 this.dataArray.push(data); 1173 this.notifyDataAdd(this.dataArray.length - 1); 1174 } 1175 1176 public reloadData(): void { 1177 this.notifyDataReload(); 1178 } 1179} 1180 1181class StringData { 1182 message: string; 1183 imgSrc: Resource; 1184 constructor(message: string, imgSrc: Resource) { 1185 this.message = message; 1186 this.imgSrc = imgSrc; 1187 } 1188} 1189 1190@Entry 1191@Component 1192struct MyComponent { 1193 private data: MyDataSource = new MyDataSource(); 1194 1195 aboutToAppear() { 1196 for (let i = 0; i <= 9; i++) { 1197 // 此处'app.media.icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。 1198 this.data.pushData(new StringData(`Click to add ${i}`, $r('app.media.icon'))); 1199 } 1200 } 1201 1202 build() { 1203 List({ space: 3 }) { 1204 LazyForEach(this.data, (item: StringData, index: number) => { 1205 ListItem() { 1206 Column() { 1207 Text(item.message).fontSize(20) 1208 .onAppear(() => { 1209 console.info("text appear:" + item.message); 1210 }) 1211 Image(item.imgSrc) 1212 .width(100) 1213 .height(100) 1214 .onAppear(() => { 1215 console.info("image appear"); 1216 }) 1217 }.margin({ left: 10, right: 10 }) 1218 } 1219 .onClick(() => { 1220 item.message += '0'; 1221 this.data.reloadData(); 1222 }) 1223 }, (item: StringData, index: number) => JSON.stringify(item)) 1224 }.cachedCount(5) 1225 } 1226} 1227``` 1228 1229上述代码运行效果如下。 1230 1231 1232 1233可以观察到在点击更改message之后,图片“闪烁”了一下,同时输出了组件的onAppear日志,这说明组件进行了重建。这是因为在更改message之后,导致LazyForEach中这一项的key值发生了变化,使得LazyForEach在reloadData的时候将这一项ListItem进行了重建。Text组件仅仅更改显示的内容却发生了重建,而不是更新。而尽管Image组件没有需要重新绘制的内容,但是因为触发LazyForEach的重建,会使得同样位于ListItem下的Image组件重新创建。 1234 1235当前LazyForEach与状态变量都能触发UI的刷新,两者的性能开销是不一样的。使用LazyForEach刷新会对组件进行重建,如果包含了多个组件,则会产生比较大的性能开销。使用状态变量刷新会对组件进行刷新,具体到状态变量关联的组件上,相对于LazyForEach的重建来说,范围更小更精确。因此,推荐使用状态变量来触发LazyForEach中的组件刷新,这就需要使用自定义组件。 1236 1237```typescript 1238class BasicDataSource implements IDataSource { 1239 private listeners: DataChangeListener[] = []; 1240 private originDataArray: StringData[] = []; 1241 1242 public totalCount(): number { 1243 return 0; 1244 } 1245 1246 public getData(index: number): StringData { 1247 return this.originDataArray[index]; 1248 } 1249 1250 registerDataChangeListener(listener: DataChangeListener): void { 1251 if (this.listeners.indexOf(listener) < 0) { 1252 console.info('add listener'); 1253 this.listeners.push(listener); 1254 } 1255 } 1256 1257 unregisterDataChangeListener(listener: DataChangeListener): void { 1258 const pos = this.listeners.indexOf(listener); 1259 if (pos >= 0) { 1260 console.info('remove listener'); 1261 this.listeners.splice(pos, 1); 1262 } 1263 } 1264 1265 notifyDataReload(): void { 1266 this.listeners.forEach(listener => { 1267 listener.onDataReloaded(); 1268 }) 1269 } 1270 1271 notifyDataAdd(index: number): void { 1272 this.listeners.forEach(listener => { 1273 listener.onDataAdd(index); 1274 }) 1275 } 1276 1277 notifyDataChange(index: number): void { 1278 this.listeners.forEach(listener => { 1279 listener.onDataChange(index); 1280 }) 1281 } 1282 1283 notifyDataDelete(index: number): void { 1284 this.listeners.forEach(listener => { 1285 listener.onDataDelete(index); 1286 }) 1287 } 1288 1289 notifyDataMove(from: number, to: number): void { 1290 this.listeners.forEach(listener => { 1291 listener.onDataMove(from, to); 1292 }) 1293 } 1294} 1295 1296class MyDataSource extends BasicDataSource { 1297 private dataArray: StringData[] = []; 1298 1299 public totalCount(): number { 1300 return this.dataArray.length; 1301 } 1302 1303 public getData(index: number): StringData { 1304 return this.dataArray[index]; 1305 } 1306 1307 public addData(index: number, data: StringData): void { 1308 this.dataArray.splice(index, 0, data); 1309 this.notifyDataAdd(index); 1310 } 1311 1312 public pushData(data: StringData): void { 1313 this.dataArray.push(data); 1314 this.notifyDataAdd(this.dataArray.length - 1); 1315 } 1316} 1317 1318@Observed 1319class StringData { 1320 @Track message: string; 1321 @Track imgSrc: Resource; 1322 constructor(message: string, imgSrc: Resource) { 1323 this.message = message; 1324 this.imgSrc = imgSrc; 1325 } 1326} 1327 1328@Entry 1329@Component 1330struct MyComponent { 1331 @State data: MyDataSource = new MyDataSource(); 1332 1333 aboutToAppear() { 1334 for (let i = 0; i <= 9; i++) { 1335 // 此处'app.media.icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。 1336 this.data.pushData(new StringData(`Click to add ${i}`, $r('app.media.icon'))); 1337 } 1338 } 1339 1340 build() { 1341 List({ space: 3 }) { 1342 LazyForEach(this.data, (item: StringData, index: number) => { 1343 ListItem() { 1344 ChildComponent({data: item}) 1345 } 1346 .onClick(() => { 1347 item.message += '0'; 1348 }) 1349 }, (item: StringData, index: number) => index.toString()) 1350 }.cachedCount(5) 1351 } 1352} 1353 1354@Component 1355struct ChildComponent { 1356 @ObjectLink data: StringData 1357 build() { 1358 Column() { 1359 Text(this.data.message).fontSize(20) 1360 .onAppear(() => { 1361 console.info("text appear:" + this.data.message); 1362 }) 1363 Image(this.data.imgSrc) 1364 .width(100) 1365 .height(100) 1366 }.margin({ left: 10, right: 10 }) 1367 } 1368} 1369``` 1370 1371上述代码运行效果如下。 1372 1373 1374 1375可以观察到UI能够正常刷新,图片没有“闪烁”,且没有输出日志信息,说明没有对Text组件和Image组件进行重建。 1376 1377这是因为使用自定义组件之后,可以通过@Observed和@ObjectLink配合去直接更改自定义组件内的状态变量实现刷新,而不需要利用LazyForEach进行重建。使用[@Track装饰器](arkts-track.md)分别装饰StringData类型中的message和imgSrc属性可以使更新范围进一步缩小到指定的Text组件。 1378 1379### 在ForEach中使用自定义组件搭配对象数组 1380 1381开发过程中经常会使用对象数组和[ForEach](arkts-rendering-control-foreach.md)结合起来使用,但是写法不当的话会出现UI不刷新的情况。 1382 1383```typescript 1384@Observed 1385class StyleList extends Array<TextStyles> { 1386}; 1387@Observed 1388class TextStyles { 1389 fontSize: number; 1390 1391 constructor(fontSize: number) { 1392 this.fontSize = fontSize; 1393 } 1394} 1395@Entry 1396@Component 1397struct Page { 1398 @State styleList: StyleList = new StyleList(); 1399 aboutToAppear() { 1400 for (let i = 15; i < 50; i++) 1401 this.styleList.push(new TextStyles(i)); 1402 } 1403 build() { 1404 Column() { 1405 Text("Font Size List") 1406 .fontSize(50) 1407 .onClick(() => { 1408 for (let i = 0; i < this.styleList.length; i++) { 1409 this.styleList[i].fontSize++; 1410 } 1411 console.info("change font size"); 1412 }) 1413 List() { 1414 ForEach(this.styleList, (item: TextStyles) => { 1415 ListItem() { 1416 Text("Hello World") 1417 .fontSize(item.fontSize) 1418 } 1419 }) 1420 } 1421 } 1422 } 1423} 1424``` 1425 1426上述代码运行效果如下。 1427 1428 1429 1430由于ForEach中生成的item是一个常量,因此当点击改变item中的内容时,没有办法观测到UI刷新,尽管日志表面item中的值已经改变了(这体现在打印了“change font size”的日志)。因此,需要使用自定义组件,配合@ObjectLink来实现观测的能力。 1431 1432```typescript 1433@Observed 1434class StyleList extends Array<TextStyles> { 1435}; 1436@Observed 1437class TextStyles { 1438 fontSize: number; 1439 1440 constructor(fontSize: number) { 1441 this.fontSize = fontSize; 1442 } 1443} 1444@Component 1445struct TextComponent { 1446 @ObjectLink textStyle: TextStyles; 1447 build() { 1448 Text("Hello World") 1449 .fontSize(this.textStyle.fontSize) 1450 } 1451} 1452@Entry 1453@Component 1454struct Page { 1455 @State styleList: StyleList = new StyleList(); 1456 aboutToAppear() { 1457 for (let i = 15; i < 50; i++) 1458 this.styleList.push(new TextStyles(i)); 1459 } 1460 build() { 1461 Column() { 1462 Text("Font Size List") 1463 .fontSize(50) 1464 .onClick(() => { 1465 for (let i = 0; i < this.styleList.length; i++) { 1466 this.styleList[i].fontSize++; 1467 } 1468 console.info("change font size"); 1469 }) 1470 List() { 1471 ForEach(this.styleList, (item: TextStyles) => { 1472 ListItem() { 1473 TextComponent({ textStyle: item}) 1474 } 1475 }) 1476 } 1477 } 1478 } 1479} 1480``` 1481 1482上述代码的运行效果如下。 1483 1484 1485 1486使用@ObjectLink接受传入的item后,使得TextComponent组件内的textStyle变量具有了被观测的能力。在父组件更改styleList中的值时,由于@ObjectLink是引用传递,所以会观测到styleList每一个数据项的地址指向的对应item的fontSize的值被改变,因此触发UI的刷新。 1487 1488这是一个较为实用的使用状态管理进行刷新的开发方式。 1489 1490 1491 1492<!--no_check-->