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