1# 精准控制组件的更新范围 2 3在复杂页面开发的场景下,精准控制组件更新的范围对提高应用运行性能尤为重要。 4 5## 多组件关联同一对象的不同属性 6 7在学习本示例之前,需要了解当前状态管理的刷新机制。 8 9```ts 10@Observed 11class ClassA { 12 prop1: number = 0; 13 prop2: string = "This is Prop2"; 14} 15@Component 16struct CompA { 17 @ObjectLink a: ClassA; 18 private sizeFont: number = 30; // the private variable does not invoke rendering 19 private isRenderText() : number { 20 this.sizeFont++; // the change of sizeFont will not invoke rendering, but showing that the function is called 21 console.info("Text prop2 is rendered"); 22 return this.sizeFont; 23 } 24 build() { 25 Column() { 26 Text(this.a.prop2) // when this.a.prop2 changes, it will invoke Text rerendering 27 .fontSize(this.isRenderText()) //if the Text renders, the function isRenderText will be called 28 } 29 } 30} 31@Entry 32@Component 33struct Page { 34 @State a: ClassA = new ClassA(); 35 build() { 36 Row() { 37 Column() { 38 Text("Prop1: " + this.a.prop1) 39 .fontSize(50) 40 .margin({ bottom: 20 }) 41 CompA({a: this.a}) 42 Button("Change prop1") 43 .width(200) 44 .margin({ top: 20 }) 45 .onClick(() => { 46 this.a.prop1 = this.a.prop1 + 1 ; 47 }) 48 } 49 .width('100%') 50 } 51 .width('100%') 52 .height('100%') 53 } 54} 55``` 56 57在上面的示例中,当点击按钮改变prop1的值时,尽管CompA中的组件并没有使用prop1,但是仍然可以观测到关联prop2的Text组件进行了刷新,这体现在Text组件的字体变大,同时控制台输出了“Text prop2 is rendered”的日志上。这说明当改变了一个由@Observed装饰的类的实例对象中的某个属性时(即上面示例中的prop1),会导致所有关联这个对象中某个属性的组件一起刷新,尽管这些组件可能并没有直接使用到该改变的属性(即上面示例中使用prop的Text组件)。这样就会导致一些隐形的“冗余刷新”,当涉及到“冗余刷新”的组件数量很多时,就会大大影响组件的刷新性能。 58 59上文代码运行图示如下: 60 61 62 63下面的示例代码为一个较典型的冗余刷新场景。 64 65```ts 66@Observed 67class UIStyle { 68 translateX: number = 0; 69 translateY: number = 0; 70 scaleX: number = 0.3; 71 scaleY: number = 0.3; 72 width: number = 336; 73 height: number = 178; 74 posX: number = 10; 75 posY: number = 50; 76 alpha: number = 0.5; 77 borderRadius: number = 24; 78 imageWidth: number = 78; 79 imageHeight: number = 78; 80 translateImageX: number = 0; 81 translateImageY: number = 0; 82 fontSize: number = 20; 83} 84@Component 85struct SpecialImage { 86 @ObjectLink uiStyle: UIStyle; 87 private isRenderSpecialImage() : number { // function to show whether the component is rendered 88 console.info("SpecialImage is rendered"); 89 return 1; 90 } 91 build() { 92 Image($r('app.media.icon')) 93 .width(this.uiStyle.imageWidth) 94 .height(this.uiStyle.imageHeight) 95 .margin({ top: 20 }) 96 .translate({ 97 x: this.uiStyle.translateImageX, 98 y: this.uiStyle.translateImageY 99 }) 100 .opacity(this.isRenderSpecialImage()) // if the Image is rendered, it will call the function 101 } 102} 103@Component 104struct CompA { 105 @ObjectLink uiStyle: UIStyle 106 // the following functions are used to show whether the component is called to be rendered 107 private isRenderColumn() : number { 108 console.info("Column is rendered"); 109 return 1; 110 } 111 private isRenderStack() : number { 112 console.info("Stack is rendered"); 113 return 1; 114 } 115 private isRenderImage() : number { 116 console.info("Image is rendered"); 117 return 1; 118 } 119 private isRenderText() : number { 120 console.info("Text is rendered"); 121 return 1; 122 } 123 build() { 124 Column() { 125 // when you compile this code in API9, IDE may tell you that 126 // "Assigning the '@ObjectLink' decorated attribute 'uiStyle' to the '@ObjectLink' decorated attribute 'uiStyle' is not allowed. <etsLint>" 127 // But you can still run the code by Previewer 128 SpecialImage({ 129 uiStyle: this.uiStyle 130 }) 131 Stack() { 132 Column() { 133 Image($r('app.media.icon')) 134 .opacity(this.uiStyle.alpha) 135 .scale({ 136 x: this.uiStyle.scaleX, 137 y: this.uiStyle.scaleY 138 }) 139 .padding(this.isRenderImage()) 140 .width(300) 141 .height(300) 142 } 143 .width('100%') 144 .position({ y: -80 }) 145 Stack() { 146 Text("Hello World") 147 .fontColor("#182431") 148 .fontWeight(FontWeight.Medium) 149 .fontSize(this.uiStyle.fontSize) 150 .opacity(this.isRenderText()) 151 .margin({ top: 12 }) 152 } 153 .opacity(this.isRenderStack()) 154 .position({ 155 x: this.uiStyle.posX, 156 y: this.uiStyle.posY 157 }) 158 .width('100%') 159 .height('100%') 160 } 161 .margin({ top: 50 }) 162 .borderRadius(this.uiStyle.borderRadius) 163 .opacity(this.isRenderStack()) 164 .backgroundColor("#FFFFFF") 165 .width(this.uiStyle.width) 166 .height(this.uiStyle.height) 167 .translate({ 168 x: this.uiStyle.translateX, 169 y: this.uiStyle.translateY 170 }) 171 Column() { 172 Button("Move") 173 .width(312) 174 .fontSize(20) 175 .backgroundColor("#FF007DFF") 176 .margin({ bottom: 10 }) 177 .onClick(() => { 178 animateTo({ 179 duration: 500 180 },() => { 181 this.uiStyle.translateY = (this.uiStyle.translateY + 180) % 250; 182 }) 183 }) 184 Button("Scale") 185 .borderRadius(20) 186 .backgroundColor("#FF007DFF") 187 .fontSize(20) 188 .width(312) 189 .onClick(() => { 190 this.uiStyle.scaleX = (this.uiStyle.scaleX + 0.6) % 0.8; 191 }) 192 } 193 .position({ 194 y:666 195 }) 196 .height('100%') 197 .width('100%') 198 199 } 200 .opacity(this.isRenderColumn()) 201 .width('100%') 202 .height('100%') 203 204 } 205} 206@Entry 207@Component 208struct Page { 209 @State uiStyle: UIStyle = new UIStyle(); 210 build() { 211 Stack() { 212 CompA({ 213 uiStyle: this.uiStyle 214 }) 215 } 216 .backgroundColor("#F1F3F5") 217 } 218} 219``` 220 221在上面的示例中,UIStyle定义了多个属性,并且这些属性分别被多个组件关联。当点击任意一个按钮更改其中的某些属性时,根据上文介绍的机制,会导致所有这些关联uiStyle的组件进行刷新,虽然它们其实并不需要进行刷新(因为组件的属性都没有改变)。通过定义的一系列isRender函数,可以观察到这些组件的刷新。当点击“move”按钮进行平移动画时,由于translateX与translateY的值的多次改变,会导致每一帧都存在冗余刷新的问题,这对应用的性能有着很大的负面影响。 222 223上文代码运行图示如下: 224 225 226 227对此,推荐将属性进行拆分,将一个大的属性对象拆分成几个小的属性对象,来减少甚至避免冗余刷新的现象,达到精准控制组件的更新范围。 228 229为了达成这一目的,首先需要了解当前属性更新观测的另一个机制。 230 231下面为示例代码。 232 233```TS 234@Observed 235class ClassB { 236 subProp1: number = 100; 237} 238@Observed 239class ClassA { 240 prop1: number = 0; 241 prop2: string = "This is Prop2"; 242 prop3: ClassB = new ClassB(); 243} 244@Component 245struct CompA { 246 @ObjectLink a: ClassA; 247 private sizeFont: number = 30; // the private variable does not invoke rendering 248 private isRenderText() : number { 249 this.sizeFont++; // the change of sizeFont will not invoke rendering, but showing that the function is called 250 console.info("Text prop2 is rendered"); 251 return this.sizeFont; 252 } 253 build() { 254 Column() { 255 Text(this.a.prop2) // when this.a.prop1 changes, it will invoke Text rerendering 256 .margin({ bottom: 10 }) 257 .fontSize(this.isRenderText()) //if the Text renders, the function isRenderText will be called 258 Text("subProp1 : " + this.a.prop3.subProp1) //the Text can not observe the change of subProp1 259 .fontSize(30) 260 } 261 } 262} 263@Entry 264@Component 265struct Page { 266 @State a: ClassA = new ClassA(); 267 build() { 268 Row() { 269 Column() { 270 Text("Prop1: " + this.a.prop1) 271 .margin({ bottom: 20 }) 272 .fontSize(50) 273 CompA({a: this.a}) 274 Button("Change prop1") 275 .width(200) 276 .fontSize(20) 277 .backgroundColor("#FF007DFF") 278 .margin({ 279 top: 10, 280 bottom: 10 281 }) 282 .onClick(() => { 283 this.a.prop1 = this.a.prop1 + 1 ; 284 }) 285 Button("Change subProp1") 286 .width(200) 287 .fontSize(20) 288 .backgroundColor("#FF007DFF") 289 .onClick(() => { 290 this.a.prop3.subProp1 = this.a.prop3.subProp1 + 1; 291 }) 292 } 293 .width('100%') 294 } 295 .width('100%') 296 .height('100%') 297 } 298} 299``` 300 301在上面的示例中,当点击按钮“Change subProp1”时,可以发现页面并没有进行刷新,这是因为对subProp1的更改并没有被组件观测到。当再次点击“Change prop1”时,可以发现页面进行了刷新,同时显示了prop1与subProp1的最新值。依据ArkUI状态管理机制,状态变量自身只能观察到第一层的变化,所以对于“Change subProp1",对第二层的属性赋值,是无法观察到的,即对this.a.prop3.subProp1的变化并不会引起组件的刷新,即使subProp1的值其实已经产生了变化。而对this.a.prop1的改变则会引起刷新。 302 303上文代码运行图示如下: 304 305 306 307利用这一个机制,可以做到精准控制组件的更新范围。 308 309```ts 310@Observed 311class ClassB { 312 subProp1: number = 100; 313} 314@Observed 315class ClassA { 316 prop1: number = 0; 317 prop2: string = "This is Prop2"; 318 prop3: ClassB = new ClassB(); 319} 320@Component 321struct CompA { 322 @ObjectLink a: ClassA; 323 @ObjectLink b: ClassB; // a new objectlink variable 324 private sizeFont: number = 30; 325 private isRenderText() : number { 326 this.sizeFont++; 327 console.info("Text prop2 is rendered"); 328 return this.sizeFont; 329 } 330 private isRenderTextSubProp1() : number { 331 this.sizeFont++; 332 console.info("Text subProp1 is rendered"); 333 return this.sizeFont; 334 } 335 build() { 336 Column() { 337 Text(this.a.prop2) // when this.a.prop1 changes, it will invoke Text rerendering 338 .margin({ bottom: 10 }) 339 .fontSize(this.isRenderText()) //if the Text renders, the function isRenderText will be called 340 Text("subProp1 : " + this.b.subProp1) // use directly b rather than a.prop3 341 .fontSize(30) 342 .opacity(this.isRenderTextSubProp1()) 343 } 344 } 345} 346@Entry 347@Component 348struct Page { 349 @State a: ClassA = new ClassA(); 350 build() { 351 Row() { 352 Column() { 353 Text("Prop1: " + this.a.prop1) 354 .margin({ bottom: 20 }) 355 .fontSize(50) 356 CompA({ 357 a: this.a, 358 b: this.a.prop3 359 }) 360 Button("Change prop1") 361 .width(200) 362 .fontSize(20) 363 .backgroundColor("#FF007DFF") 364 .margin({ 365 top: 10, 366 bottom: 10 367 }) 368 .onClick(() => { 369 this.a.prop1 = this.a.prop1 + 1 ; 370 }) 371 Button("Change subProp1") 372 .width(200) 373 .fontSize(20) 374 .backgroundColor("#FF007DFF") 375 .margin({ 376 top: 10, 377 bottom: 10 378 }) 379 .onClick(() => { 380 this.a.prop3.subProp1 = this.a.prop3.subProp1 + 1; 381 }) 382 } 383 .width('100%') 384 } 385 .width('100%') 386 .height('100%') 387 } 388} 389``` 390 391在上面的示例中,在CompA中定义了一个新的ObjectLink装饰的变量b,并由Page创建CompA时,将a对象中的prop3传入给b,这样就能在子组件CompA中直接使用b,这使得组件实际上和b进行了关联,组件也就能观测到b中的subProp1的变化,当点击按钮“Change subProp1”的时时候,可以只触发相关联的Text的组件的刷新,而不会引起其他的组件刷新(因为其他组件关联的是a),同样的其他对于a中属性的修改也不会导致该Text组件的刷新。 392 393上文代码运行图示如下: 394 395 396 397通过这个方法,可以将上文的复杂冗余刷新场景进行属性拆分实现性能优化。 398 399```ts 400@Observed 401class NeedRenderImage { // properties only used in the same component can be divided into the same new divided class 402 public translateImageX: number = 0; 403 public translateImageY: number = 0; 404 public imageWidth:number = 78; 405 public imageHeight:number = 78; 406} 407@Observed 408class NeedRenderScale { // properties usually used together can be divided into the same new divided class 409 public scaleX: number = 0.3; 410 public scaleY: number = 0.3; 411} 412@Observed 413class NeedRenderAlpha { // properties that may be used in different places can be divided into the same new divided class 414 public alpha: number = 0.5; 415} 416@Observed 417class NeedRenderSize { // properties usually used together can be divided into the same new divided class 418 public width: number = 336; 419 public height: number = 178; 420} 421@Observed 422class NeedRenderPos { // properties usually used together can be divided into the same new divided class 423 public posX: number = 10; 424 public posY: number = 50; 425} 426@Observed 427class NeedRenderBorderRadius { // properties that may be used in different places can be divided into the same new divided class 428 public borderRadius: number = 24; 429} 430@Observed 431class NeedRenderFontSize { // properties that may be used in different places can be divided into the same new divided class 432 public fontSize: number = 20; 433} 434@Observed 435class NeedRenderTranslate { // properties usually used together can be divided into the same new divided class 436 public translateX: number = 0; 437 public translateY: number = 0; 438} 439@Observed 440class UIStyle { 441 // define new variable instead of using old one 442 needRenderTranslate: NeedRenderTranslate = new NeedRenderTranslate(); 443 needRenderFontSize: NeedRenderFontSize = new NeedRenderFontSize(); 444 needRenderBorderRadius: NeedRenderBorderRadius = new NeedRenderBorderRadius(); 445 needRenderPos: NeedRenderPos = new NeedRenderPos(); 446 needRenderSize: NeedRenderSize = new NeedRenderSize(); 447 needRenderAlpha: NeedRenderAlpha = new NeedRenderAlpha(); 448 needRenderScale: NeedRenderScale = new NeedRenderScale(); 449 needRenderImage: NeedRenderImage = new NeedRenderImage(); 450} 451@Component 452struct SpecialImage { 453 @ObjectLink uiStyle : UIStyle; 454 @ObjectLink needRenderImage: NeedRenderImage // receive the new class from its parent component 455 private isRenderSpecialImage() : number { // function to show whether the component is rendered 456 console.info("SpecialImage is rendered"); 457 return 1; 458 } 459 build() { 460 Image($r('app.media.icon')) 461 .width(this.needRenderImage.imageWidth) // !! use this.needRenderImage.xxx rather than this.uiStyle.needRenderImage.xxx !! 462 .height(this.needRenderImage.imageHeight) 463 .margin({top:20}) 464 .translate({ 465 x: this.needRenderImage.translateImageX, 466 y: this.needRenderImage.translateImageY 467 }) 468 .opacity(this.isRenderSpecialImage()) // if the Image is rendered, it will call the function 469 } 470} 471@Component 472struct CompA { 473 @ObjectLink uiStyle: UIStyle; 474 @ObjectLink needRenderTranslate: NeedRenderTranslate; // receive the new class from its parent component 475 @ObjectLink needRenderFontSize: NeedRenderFontSize; 476 @ObjectLink needRenderBorderRadius: NeedRenderBorderRadius; 477 @ObjectLink needRenderPos: NeedRenderPos; 478 @ObjectLink needRenderSize: NeedRenderSize; 479 @ObjectLink needRenderAlpha: NeedRenderAlpha; 480 @ObjectLink needRenderScale: NeedRenderScale; 481 // the following functions are used to show whether the component is called to be rendered 482 private isRenderColumn() : number { 483 console.info("Column is rendered"); 484 return 1; 485 } 486 private isRenderStack() : number { 487 console.info("Stack is rendered"); 488 return 1; 489 } 490 private isRenderImage() : number { 491 console.info("Image is rendered"); 492 return 1; 493 } 494 private isRenderText() : number { 495 console.info("Text is rendered"); 496 return 1; 497 } 498 build() { 499 Column() { 500 // when you compile this code in API9, IDE may tell you that 501 // "Assigning the '@ObjectLink' decorated attribute 'uiStyle' to the '@ObjectLink' decorated attribute 'uiStyle' is not allowed. <etsLint>" 502 // "Assigning the '@ObjectLink' decorated attribute 'uiStyle' to the '@ObjectLink' decorated attribute 'needRenderImage' is not allowed. <etsLint>" 503 // But you can still run the code by Previewer 504 SpecialImage({ 505 uiStyle: this.uiStyle, 506 needRenderImage: this.uiStyle.needRenderImage //send it to its child 507 }) 508 Stack() { 509 Column() { 510 Image($r('app.media.icon')) 511 .opacity(this.needRenderAlpha.alpha) 512 .scale({ 513 x: this.needRenderScale.scaleX, // use this.needRenderXxx.xxx rather than this.uiStyle.needRenderXxx.xxx 514 y: this.needRenderScale.scaleY 515 }) 516 .padding(this.isRenderImage()) 517 .width(300) 518 .height(300) 519 } 520 .width('100%') 521 .position({ y: -80 }) 522 523 Stack() { 524 Text("Hello World") 525 .fontColor("#182431") 526 .fontWeight(FontWeight.Medium) 527 .fontSize(this.needRenderFontSize.fontSize) 528 .opacity(this.isRenderText()) 529 .margin({ top: 12 }) 530 } 531 .opacity(this.isRenderStack()) 532 .position({ 533 x: this.needRenderPos.posX, 534 y: this.needRenderPos.posY 535 }) 536 .width('100%') 537 .height('100%') 538 } 539 .margin({ top: 50 }) 540 .borderRadius(this.needRenderBorderRadius.borderRadius) 541 .opacity(this.isRenderStack()) 542 .backgroundColor("#FFFFFF") 543 .width(this.needRenderSize.width) 544 .height(this.needRenderSize.height) 545 .translate({ 546 x: this.needRenderTranslate.translateX, 547 y: this.needRenderTranslate.translateY 548 }) 549 550 Column() { 551 Button("Move") 552 .width(312) 553 .fontSize(20) 554 .backgroundColor("#FF007DFF") 555 .margin({ bottom: 10 }) 556 .onClick(() => { 557 animateTo({ 558 duration: 500 559 }, () => { 560 this.needRenderTranslate.translateY = (this.needRenderTranslate.translateY + 180) % 250; 561 }) 562 }) 563 Button("Scale") 564 .borderRadius(20) 565 .backgroundColor("#FF007DFF") 566 .fontSize(20) 567 .width(312) 568 .margin({ bottom: 10 }) 569 .onClick(() => { 570 this.needRenderScale.scaleX = (this.needRenderScale.scaleX + 0.6) % 0.8; 571 }) 572 Button("Change Image") 573 .borderRadius(20) 574 .backgroundColor("#FF007DFF") 575 .fontSize(20) 576 .width(312) 577 .onClick(() => { // in the parent component, still use this.uiStyle.needRenderXxx.xxx to change the properties 578 this.uiStyle.needRenderImage.imageWidth = (this.uiStyle.needRenderImage.imageWidth + 30) % 160; 579 this.uiStyle.needRenderImage.imageHeight = (this.uiStyle.needRenderImage.imageHeight + 30) % 160; 580 }) 581 } 582 .position({ 583 y: 616 584 }) 585 .height('100%') 586 .width('100%') 587 } 588 .opacity(this.isRenderColumn()) 589 .width('100%') 590 .height('100%') 591 } 592} 593@Entry 594@Component 595struct Page { 596 @State uiStyle: UIStyle = new UIStyle(); 597 build() { 598 Stack() { 599 CompA({ 600 uiStyle: this.uiStyle, 601 needRenderTranslate: this.uiStyle.needRenderTranslate, //send all the new class child need 602 needRenderFontSize: this.uiStyle.needRenderFontSize, 603 needRenderBorderRadius: this.uiStyle.needRenderBorderRadius, 604 needRenderPos: this.uiStyle.needRenderPos, 605 needRenderSize: this.uiStyle.needRenderSize, 606 needRenderAlpha: this.uiStyle.needRenderAlpha, 607 needRenderScale: this.uiStyle.needRenderScale 608 }) 609 } 610 .backgroundColor("#F1F3F5") 611 } 612} 613``` 614 615上文代码运行图示如下: 616 617 618 619可以使用SmartPerf Host工具分别抓取优化前后点击“move”按钮时的trace数据,来查看属性拆分的性能收益。 620 621优化前点击move按钮的脏节点更新耗时如下图: 622 623 624 625优化后点击move按钮的脏节点更新耗时如下图: 626 627 628 629从上面trace图中的“H:FlushDirtyNodeUpdate”标签可以看出,优化前点击“move”按钮的脏节点更新耗时为1ms416μs,而通过拆分属性进行优化后耗时仅836μs,性能提升了大约40.9%。 630 631在上面的示例中将原先大类中的十五个属性拆成了八个小类,并且在组件的属性绑定中也进行了相应的适配。属性拆分遵循以下几点原则: 632 633- 只作用在同一个组件上的多个属性可以被拆分进同一个新类,即示例中的NeedRenderImage。适用于组件经常被不关联的属性改变而引起刷新的场景,这个时候就要考虑拆分属性,或者重新考虑ViewModel设计是否合理。 634- 经常被同时使用的属性可以被拆分进同一个新类,即示例中的NeedRenderScale、NeedRenderTranslate、NeedRenderPos、NeedRenderSize。适用于属性经常成对出现,或者被作用在同一个样式上的情况,例如.translate、.position、.scale等(这些样式通常会接收一个对象作为参数)。 635- 可能被用在多个组件上或相对较独立的属性应该被单独拆分进一个新类,即示例中的NeedRenderAlpha,NeedRenderBorderRadius、NeedRenderFontSize。适用于一个属性作用在多个组件上或者与其他属性没有联系的情况,例如.opacity、.borderRadius等(这些样式通常相对独立)。 636 637在对属性进行拆分后,对所有使用属性对组件进行绑定的时候,需要使用以下格式: 638 639```ts 640.property(this.needRenderXxx.xxx) 641 642// sample 643Text("some text") 644.width(this.needRenderSize.width) 645.height(this.needRenderSize.height) 646.opacity(this.needRenderAlpha.alpha) 647``` 648 649在父组件改变属性的值时,可以通过外层的父类去修改,即: 650 651```ts 652// in parent Component 653this.parent.needRenderXxx.xxx = x; 654 655//example 656this.uiStyle.needRenderImage.imageWidth = (this.uiStyle.needRenderImage.imageWidth + 20) % 60; 657``` 658 659在子组件本身改变属性的值时,推荐直接通过新类去修改,即: 660 661```ts 662// in child Component 663this.needRenderXxx.xxx = x; 664 665//example 666this.needRenderScale.scaleX = (this.needRenderScale.scaleX + 0.6) % 1 667``` 668 669属性拆分应当重点考虑变化较为频繁的属性,来提高应用运行的性能。 670 671如果想要在父组件中使用拆分后的属性,推荐新定义一个@State修饰的状态变量配合使用。 672 673```ts 674@Observed 675class NeedRenderProperty { 676 public property: number = 1; 677}; 678@Observed 679class SomeClass { 680 needRenderProperty: NeedRenderProperty = new NeedRenderProperty(); 681} 682@Entry 683@Component 684struct Page { 685 @State someClass: SomeClass = new SomeClass(); 686 @State needRenderProperty: NeedRenderProperty = this.someClass.needRenderProperty 687 build() { 688 Row() { 689 Column() { 690 Text("property value: " + this.needRenderProperty.property) 691 .fontSize(30) 692 .margin({ bottom: 20 }) 693 Button("Change property") 694 .onClick(() => { 695 this.needRenderProperty.property++; 696 }) 697 } 698 .width('100%') 699 } 700 .width('100%') 701 .height('100%') 702 } 703} 704``` 705 706## 多组件关联同一数据的条件刷新 707 708多个组件依赖对象中的不同属性时,直接关联该对象会出现改变任一属性所有组件都刷新的现象,可以通过将类中的属性拆分组合成新类的方式精准控制组件刷新。 709 710在多个组件依赖同一个数据源并根据数据源变化刷新组件的情况下,直接关联数据源会导致每次数据源改变都刷新所有组件。为精准控制组件刷新,可以采取以下策略: 711 1. 使用 [@Watch](../quick-start/arkts-watch.md) 装饰器:在组件中使用@Watch装饰器监听数据源,当数据变化时执行业务逻辑,确保只有满足条件的组件进行刷新。 712 2. 事件驱动更新:对于复杂组件关系或跨层级情况,使用[Emitter](../reference/apis-basic-services-kit/js-apis-emitter.md)自定义事件发布订阅机制。数据源变化时触发相应事件,订阅该事件的组件接收到通知后,根据变化的具体值判断组件是否刷新。 713 714【反例】 715 716在下面的示例代码中,多个组件直接关联同一个数据源,但是未使用@Watch装饰器和Emitter事件驱动更新,导致了冗余的组件刷新。 717 718```ts 719@Entry 720@Component 721struct Index { 722 @State currentIndex: number = 0; // 当前选中的列表项下标 723 private listData: string[] = []; 724 725 aboutToAppear(): void { 726 for (let i = 0; i < 10; i++) { 727 this.listData.push(`组件 ${i}`); 728 } 729 } 730 731 build() { 732 Row() { 733 Column() { 734 List() { 735 ForEach(this.listData, (item: string, index: number) => { 736 ListItem() { 737 ListItemComponent({ item: item, index: index, currentIndex: this.currentIndex }) 738 } 739 }) 740 } 741 .alignListItem(ListItemAlign.Center) 742 } 743 .width('100%') 744 } 745 .height('100%') 746 } 747} 748 749@Component 750struct ListItemComponent { 751 @Prop item: string; 752 @Prop index: number; // 列表项的下标 753 @Link currentIndex: number; 754 private sizeFont: number = 50; 755 756 isRender(): number { 757 console.info(`ListItemComponent ${this.index} Text is rendered`); 758 return this.sizeFont; 759 } 760 761 build() { 762 Column() { 763 Text(this.item) 764 .fontSize(this.isRender()) 765 // 根据当前列表项下标index与currentIndex的差值来动态设置文本的颜色 766 .fontColor(Math.abs(this.index - this.currentIndex) <= 1 ? Color.Red : Color.Blue) 767 .onClick(() => { 768 this.currentIndex = this.index; 769 }) 770 } 771 } 772} 773``` 774上述示例中,每个ListItemComponent组件点击Text后会将当前点击的列表项下标index赋值给currentIndex,@Link装饰的状态变量currentIndex会将变化传递给父组件Index和所有ListItemComponent组件。然后,在所有ListItemComponent组件中,根据列表项下标index与currentIndex的差值的绝对值是否小于等于1来决定Text的颜色,如果满足条件,则文本显示为红色,否则显示为蓝色。 775 776下面是运行效果图。 777 778 779可以看到每次点击后即使其中部分Text组件的颜色并没有发生改变,所有的Text组件也都会刷新。这是由于ListItemComponent组件中的Text组件直接关联了currentIndex,而不是根据currentIndex计算得到的颜色。 780 781针对上述父子组件层级关系的场景,推荐使用状态装饰器@Watch监听数据源。当数据源改变时,在@Watch的监听回调中执行业务逻辑。组件关联回调的处理结果,而不是直接关联数据源。 782 783【正例】 784 785下面是对上述示例的优化,展示如何通过@Watch装饰器实现精准刷新。 786 787```ts 788@Entry 789@Component 790struct Index { 791 @State currentIndex: number = 0; // 当前选中的列表项下标 792 private listData: string[] = []; 793 794 795 aboutToAppear(): void { 796 for (let i = 0; i < 10; i++) { 797 this.listData.push(`组件 ${i}`); 798 } 799 } 800 801 build() { 802 Row() { 803 Column() { 804 List() { 805 ForEach(this.listData, (item: string, index: number) => { 806 ListItem() { 807 ListItemComponent({ item: item, index: index, currentIndex: this.currentIndex }) 808 } 809 }) 810 } 811 .alignListItem(ListItemAlign.Center) 812 } 813 .width('100%') 814 } 815 .height('100%') 816 } 817} 818 819@Component 820struct ListItemComponent { 821 @Prop item: string; 822 @Prop index: number; // 列表项的下标 823 @Link @Watch('onCurrentIndexUpdate') currentIndex: number; 824 @State color: Color = Math.abs(this.index - this.currentIndex) <= 1 ? Color.Red : Color.Blue; 825 826 isRender(): number { 827 console.info(`ListItemComponent ${this.index} Text is rendered`); 828 return 50; 829 } 830 831 onCurrentIndexUpdate() { 832 // 根据当前列表项下标index与currentIndex的差值来动态修改color的值 833 this.color = Math.abs(this.index - this.currentIndex) <= 1 ? Color.Red : Color.Blue; 834 } 835 836 build() { 837 Column() { 838 Text(this.item) 839 .fontSize(this.isRender()) 840 .fontColor(this.color) 841 .onClick(() => { 842 this.currentIndex = this.index; 843 }) 844 } 845 } 846} 847``` 848上述代码中,ListItemComponent组件中的状态变量currentIndex使用@Watch装饰,Text组件直接关联新的状态变量color。当currentIndex发生变化时,会触发onCurrentIndexUpdate方法,在其中将表达式的运算结果赋值给状态变量color。只有color的值发生变化时,Text组件才会重新渲染。 849 850运行效果图如下。 851 852 853 854【效果对比】 855 856使用SmartPerf Host工具分别抓取正反例中切换选中项的trace数据,通过点击组件2的脏节点更新耗时分析二者的性能差异。 857 858反例点击组件2的脏节点更新耗时如下图: 859 860 861 862正例点击组件2的脏节点更新耗时如下图: 863 864 865 866从上面的trace图可以看出,反例中点击组件2后十个ListItemComponent组件节点都触发了更新,脏节点更新耗时3ms179μs,而正例只有三个节点触发更新,脏节点更新耗时仅为1ms600μs,性能提升了大约49.7%。 867 868被依赖的数据源仅在父子或兄弟关系的组件中传递时,可以参考上述示例,使用@State/@Link/@Watch装饰器进行状态管理,实现组件的精准刷新。 869 870当组件关系层级较多但都归属于同一个确定的组件树时,推荐使用@Provide/@Consume传递数据,使用@Watch装饰器监听数据变化,在监听回调中执行业务逻辑。参考如下伪代码。 871 872```ts 873// in ParentComponent 874@Provide @Watch('onCurrentValueUpdate') currentValue: number = 0; 875@State parentComponentResult: number = 0; 876onCurrentValueUpdate() { 877 // 执行业务逻辑 878 this.parentComponentResult = X; // X 代表基于业务逻辑需要赋给parentComponentResult的值 879} 880Component.property(this.parentComponentResult) 881 882// in ChildComponent 883@Provide @Watch('onCurrentValueUpdate') currentValue: number = 0; 884@State childComponentResult: number = 0; 885onCurrentValueUpdate() { 886 // 执行业务逻辑 887 this.childComponentResult = X; // X 代表基于业务逻辑需要赋给childComponentResult的值 888} 889Component.property(this.childComponentResult) 890 891// in NestedComponent 892@Provide @Watch('onCurrentValueUpdate') currentValue: number = 0; 893@State nestedComponentResult: number = 0; 894onCurrentValueUpdate() { 895 // 执行业务逻辑 896 this.nestedComponentResult = X; // X 代表基于业务逻辑需要赋给nestedComponentResult的值 897} 898Component.property(this.nestedComponentResult) 899``` 900 901当组件关系复杂或跨越层级过多时,推荐使用 [Emitter](../reference/apis-basic-services-kit/js-apis-emitter.md) 自定义事件发布订阅的方式。当数据源改变时发布事件,依赖该数据源的组件通过订阅事件来获取数据源的改变,完成业务逻辑的处理,从而实现组件的精准刷新。 902 903下面通过部分示例代码介绍使用方式。 904 905ButtonComponent组件作为交互组件触发数据变更,ListItemComponent组件接收数据做相应的UI刷新。 906 907```ts 908Column() { 909 Row() { 910 Column() { 911 ButtonComponent() 912 } 913 } 914 Column() { 915 Column() { 916 List() { 917 ForEach(this.listData, (item: string, index: number) => { 918 ListItemComponent({ myItem: item, index: index }) 919 }) 920 } 921 .alignListItem(ListItemAlign.Center) 922 } 923 } 924} 925``` 926由于ButtonComponent组件和ListItemComponent组件的组件关系较为复杂,因此在ButtonComponent组件中的Button回调中,可以使用emitter.emit发送事件,在ListItemComponent组件中订阅事件。在事件触发的回调中接收数据value,通过业务逻辑决定是否修改状态变量color,从而实现精准控制ListItemComponent组件中Text的刷新。 927 928```ts 929// in ButtonComponent 930Button(`下标是${this.value}的倍数的组件文字变为红色`) 931 .onClick(() => { 932 let event: emitter.InnerEvent = { 933 eventId: 1, 934 priority: emitter.EventPriority.LOW 935 }; 936 let eventData: emitter.EventData = { 937 data: { 938 value: this.value 939 } 940 }; 941 // 发送eventId为1的事件,事件内容为eventData 942 emitter.emit(event, eventData); 943 this.value++; 944 }) 945 946``` 947 948```ts 949// in ListItemComponent 950@State color: Color = Color.Black; 951aboutToAppear(): void { 952 let event: emitter.InnerEvent = { 953 eventId: 1 954 }; 955 // 收到eventId为1的事件后执行该回调 956 let callback = (eventData: emitter.EventData): void => { 957 if (eventData.data?.value !== 0 && this.index % eventData.data?.value === 0) { 958 this.color = Color.Red; 959 } 960 }; 961 // 订阅eventId为1的事件 962 emitter.on(event, callback); 963} 964build() { 965 Column() { 966 Text(this.myItem) 967 .fontSize(this.isRender()) 968 .fontColor(this.color) 969 } 970} 971```