1# 精准控制组件的更新范围 2 3在复杂页面开发的场景下,精准控制组件更新的范围对提高应用运行性能尤为重要。 4 5在学习本示例之前,需要了解当前状态管理的刷新机制。 6 7```ts 8@Observed 9class ClassA { 10 prop1: number = 0; 11 prop2: string = "This is Prop2"; 12} 13@Component 14struct CompA { 15 @ObjectLink a: ClassA; 16 private sizeFont: number = 30; // the private variable does not invoke rendering 17 private isRenderText() : number { 18 this.sizeFont++; // the change of sizeFont will not invoke rendering, but showing that the function is called 19 console.log("Text prop2 is rendered"); 20 return this.sizeFont; 21 } 22 build() { 23 Column() { 24 Text(this.a.prop2) // when this.a.prop2 changes, it will invoke Text rerendering 25 .fontSize(this.isRenderText()) //if the Text renders, the function isRenderText will be called 26 } 27 } 28} 29@Entry 30@Component 31struct Page { 32 @State a: ClassA = new ClassA(); 33 build() { 34 Row() { 35 Column() { 36 Text("Prop1: " + this.a.prop1) 37 .fontSize(50) 38 .margin({ bottom: 20 }) 39 CompA({a: this.a}) 40 Button("Change prop1") 41 .width(200) 42 .margin({ top: 20 }) 43 .onClick(() => { 44 this.a.prop1 = this.a.prop1 + 1 ; 45 }) 46 } 47 .width('100%') 48 } 49 .width('100%') 50 .height('100%') 51 } 52} 53``` 54 55在上面的示例中,当点击按钮改变prop1的值时,尽管CompA中的组件并没有使用prop1,但是仍然可以观测到关联prop2的Text组件进行了刷新,这体现在Text组件的字体变大,同时控制台输出了“Text prop2 is rendered”的日志上。这说明当改变了一个由@Observed装饰的类的实例对象中的某个属性时(即上面示例中的prop1),会导致所有关联这个对象中某个属性的组件一起刷新,尽管这些组件可能并没有直接使用到该改变的属性(即上面示例中使用prop的Text组件)。这样就会导致一些隐形的“冗余刷新”,当涉及到“冗余刷新”的组件数量很多时,就会大大影响组件的刷新性能。 56 57上文代码运行图示如下: 58 59 60 61下面的示例代码为一个较典型的冗余刷新场景。 62 63```ts 64@Observed 65class UIStyle { 66 translateX: number = 0; 67 translateY: number = 0; 68 scaleX: number = 0.3; 69 scaleY: number = 0.3; 70 width: number = 336; 71 height: number = 178; 72 posX: number = 10; 73 posY: number = 50; 74 alpha: number = 0.5; 75 borderRadius: number = 24; 76 imageWidth: number = 78; 77 imageHeight: number = 78; 78 translateImageX: number = 0; 79 translateImageY: number = 0; 80 fontSize: number = 20; 81} 82@Component 83struct SpecialImage { 84 @ObjectLink uiStyle: UIStyle; 85 private isRenderSpecialImage() : number { // function to show whether the component is rendered 86 console.log("SpecialImage is rendered"); 87 return 1; 88 } 89 build() { 90 Image($r('app.media.icon')) 91 .width(this.uiStyle.imageWidth) 92 .height(this.uiStyle.imageHeight) 93 .margin({ top: 20 }) 94 .translate({ 95 x: this.uiStyle.translateImageX, 96 y: this.uiStyle.translateImageY 97 }) 98 .opacity(this.isRenderSpecialImage()) // if the Image is rendered, it will call the function 99 } 100} 101@Component 102struct CompA { 103 @ObjectLink uiStyle: UIStyle 104 // the following functions are used to show whether the component is called to be rendered 105 private isRenderColumn() : number { 106 console.log("Column is rendered"); 107 return 1; 108 } 109 private isRenderStack() : number { 110 console.log("Stack is rendered"); 111 return 1; 112 } 113 private isRenderImage() : number { 114 console.log("Image is rendered"); 115 return 1; 116 } 117 private isRenderText() : number { 118 console.log("Text is rendered"); 119 return 1; 120 } 121 build() { 122 Column() { 123 // when you compile this code in API9, IDE may tell you that 124 // "Assigning the '@ObjectLink' decorated attribute 'uiStyle' to the '@ObjectLink' decorated attribute 'uiStyle' is not allowed. <etsLint>" 125 // But you can still run the code by Previewer 126 SpecialImage({ 127 uiStyle: this.uiStyle 128 }) 129 Stack() { 130 Column() { 131 Image($r('app.media.icon')) 132 .opacity(this.uiStyle.alpha) 133 .scale({ 134 x: this.uiStyle.scaleX, 135 y: this.uiStyle.scaleY 136 }) 137 .padding(this.isRenderImage()) 138 .width(300) 139 .height(300) 140 } 141 .width('100%') 142 .position({ y: -80 }) 143 Stack() { 144 Text("Hello World") 145 .fontColor("#182431") 146 .fontWeight(FontWeight.Medium) 147 .fontSize(this.uiStyle.fontSize) 148 .opacity(this.isRenderText()) 149 .margin({ top: 12 }) 150 } 151 .opacity(this.isRenderStack()) 152 .position({ 153 x: this.uiStyle.posX, 154 y: this.uiStyle.posY 155 }) 156 .width('100%') 157 .height('100%') 158 } 159 .margin({ top: 50 }) 160 .borderRadius(this.uiStyle.borderRadius) 161 .opacity(this.isRenderStack()) 162 .backgroundColor("#FFFFFF") 163 .width(this.uiStyle.width) 164 .height(this.uiStyle.height) 165 .translate({ 166 x: this.uiStyle.translateX, 167 y: this.uiStyle.translateY 168 }) 169 Column() { 170 Button("Move") 171 .width(312) 172 .fontSize(20) 173 .backgroundColor("#FF007DFF") 174 .margin({ bottom: 10 }) 175 .onClick(() => { 176 animateTo({ 177 duration: 500 178 },() => { 179 this.uiStyle.translateY = (this.uiStyle.translateY + 180) % 250; 180 }) 181 }) 182 Button("Scale") 183 .borderRadius(20) 184 .backgroundColor("#FF007DFF") 185 .fontSize(20) 186 .width(312) 187 .onClick(() => { 188 this.uiStyle.scaleX = (this.uiStyle.scaleX + 0.6) % 0.8; 189 }) 190 } 191 .position({ 192 y:666 193 }) 194 .height('100%') 195 .width('100%') 196 197 } 198 .opacity(this.isRenderColumn()) 199 .width('100%') 200 .height('100%') 201 202 } 203} 204@Entry 205@Component 206struct Page { 207 @State uiStyle: UIStyle = new UIStyle(); 208 build() { 209 Stack() { 210 CompA({ 211 uiStyle: this.uiStyle 212 }) 213 } 214 .backgroundColor("#F1F3F5") 215 } 216} 217``` 218 219在上面的示例中,UIStyle定义了多个属性,并且这些属性分别被多个组件关联。当点击任意一个按钮更改其中的某些属性时,根据上文介绍的机制,会导致所有这些关联uiStyle的组件进行刷新,虽然它们其实并不需要进行刷新(因为组件的属性都没有改变)。通过定义的一系列isRender函数,可以观察到这些组件的刷新。当点击“move”按钮进行平移动画时,由于translateX与translateY的值的多次改变,会导致每一帧都存在冗余刷新的问题,这对应用的性能有着很大的负面影响。 220 221上文代码运行图示如下: 222 223 224 225对此,推荐将属性进行拆分,将一个大的属性对象拆分成几个小的属性对象,来减少甚至避免冗余刷新的现象,达到精准控制组件的更新范围。 226 227为了达成这一目的,首先需要了解当前属性更新观测的另一个机制。 228 229下面为示例代码。 230 231```TS 232@Observed 233class ClassB { 234 subProp1: number = 100; 235} 236@Observed 237class ClassA { 238 prop1: number = 0; 239 prop2: string = "This is Prop2"; 240 prop3: ClassB = new ClassB(); 241} 242@Component 243struct CompA { 244 @ObjectLink a: ClassA; 245 private sizeFont: number = 30; // the private variable does not invoke rendering 246 private isRenderText() : number { 247 this.sizeFont++; // the change of sizeFont will not invoke rendering, but showing that the function is called 248 console.log("Text prop2 is rendered"); 249 return this.sizeFont; 250 } 251 build() { 252 Column() { 253 Text(this.a.prop2) // when this.a.prop1 changes, it will invoke Text rerendering 254 .margin({ bottom: 10 }) 255 .fontSize(this.isRenderText()) //if the Text renders, the function isRenderText will be called 256 Text("subProp1 : " + this.a.prop3.subProp1) //the Text can not observe the change of subProp1 257 .fontSize(30) 258 } 259 } 260} 261@Entry 262@Component 263struct Page { 264 @State a: ClassA = new ClassA(); 265 build() { 266 Row() { 267 Column() { 268 Text("Prop1: " + this.a.prop1) 269 .margin({ bottom: 20 }) 270 .fontSize(50) 271 CompA({a: this.a}) 272 Button("Change prop1") 273 .width(200) 274 .fontSize(20) 275 .backgroundColor("#FF007DFF") 276 .margin({ 277 top: 10, 278 bottom: 10 279 }) 280 .onClick(() => { 281 this.a.prop1 = this.a.prop1 + 1 ; 282 }) 283 Button("Change subProp1") 284 .width(200) 285 .fontSize(20) 286 .backgroundColor("#FF007DFF") 287 .onClick(() => { 288 this.a.prop3.subProp1 = this.a.prop3.subProp1 + 1; 289 }) 290 } 291 .width('100%') 292 } 293 .width('100%') 294 .height('100%') 295 } 296} 297``` 298 299在上面的示例中,当点击按钮“Change subProp1”时,可以发现页面并没有进行刷新,这是因为对subProp1的更改并没有被组件观测到。当再次点击“Change prop1”时,可以发现页面进行了刷新,同时显示了prop1与subProp1的最新值。依据ArkUI状态管理机制,状态变量自身只能观察到第一层的变化,所以对于“Change subProp1",对第二层的属性赋值,是无法观察到的,即对this.a.prop3.subProp1的变化并不会引起组件的刷新,即使subProp1的值其实已经产生了变化。而对this.a.prop1的改变则会引起刷新。 300 301上文代码运行图示如下: 302 303 304 305利用这一个机制,可以做到精准控制组件的更新范围。 306 307```ts 308@Observed 309class ClassB { 310 subProp1: number = 100; 311} 312@Observed 313class ClassA { 314 prop1: number = 0; 315 prop2: string = "This is Prop2"; 316 prop3: ClassB = new ClassB(); 317} 318@Component 319struct CompA { 320 @ObjectLink a: ClassA; 321 @ObjectLink b: ClassB; // a new objectlink variable 322 private sizeFont: number = 30; 323 private isRenderText() : number { 324 this.sizeFont++; 325 console.log("Text prop2 is rendered"); 326 return this.sizeFont; 327 } 328 private isRenderTextSubProp1() : number { 329 this.sizeFont++; 330 console.log("Text subProp1 is rendered"); 331 return this.sizeFont; 332 } 333 build() { 334 Column() { 335 Text(this.a.prop2) // when this.a.prop1 changes, it will invoke Text rerendering 336 .margin({ bottom: 10 }) 337 .fontSize(this.isRenderText()) //if the Text renders, the function isRenderText will be called 338 Text("subProp1 : " + this.b.subProp1) // use directly b rather than a.prop3 339 .fontSize(30) 340 .opacity(this.isRenderTextSubProp1()) 341 } 342 } 343} 344@Entry 345@Component 346struct Page { 347 @State a: ClassA = new ClassA(); 348 build() { 349 Row() { 350 Column() { 351 Text("Prop1: " + this.a.prop1) 352 .margin({ bottom: 20 }) 353 .fontSize(50) 354 CompA({ 355 a: this.a, 356 b: this.a.prop3 357 }) 358 Button("Change prop1") 359 .width(200) 360 .fontSize(20) 361 .backgroundColor("#FF007DFF") 362 .margin({ 363 top: 10, 364 bottom: 10 365 }) 366 .onClick(() => { 367 this.a.prop1 = this.a.prop1 + 1 ; 368 }) 369 Button("Change subProp1") 370 .width(200) 371 .fontSize(20) 372 .backgroundColor("#FF007DFF") 373 .margin({ 374 top: 10, 375 bottom: 10 376 }) 377 .onClick(() => { 378 this.a.prop3.subProp1 = this.a.prop3.subProp1 + 1; 379 }) 380 } 381 .width('100%') 382 } 383 .width('100%') 384 .height('100%') 385 } 386} 387``` 388 389在上面的示例中,在CompA中定义了一个新的ObjectLink装饰的变量b,并由Page创建CompA时,将a对象中的prop3传入给b,这样就能在子组件CompA中直接使用b,这使得组件实际上和b进行了关联,组件也就能观测到b中的subProp1的变化,当点击按钮“Change subProp1”的时时候,可以只触发相关联的Text的组件的刷新,而不会引起其他的组件刷新(因为其他组件关联的是a),同样的其他对于a中属性的修改也不会导致该Text组件的刷新。 390 391上文代码运行图示如下: 392 393 394 395通过这个方法,可以将上文的复杂冗余刷新场景进行属性拆分实现性能优化。 396 397```ts 398@Observed 399class NeedRenderImage { // properties only used in the same component can be divided into the same new divided class 400 public translateImageX: number = 0; 401 public translateImageY: number = 0; 402 public imageWidth:number = 78; 403 public imageHeight:number = 78; 404} 405@Observed 406class NeedRenderScale { // properties usually used together can be divided into the same new divided class 407 public scaleX: number = 0.3; 408 public scaleY: number = 0.3; 409} 410@Observed 411class NeedRenderAlpha { // properties that may be used in different places can be divided into the same new divided class 412 public alpha: number = 0.5; 413} 414@Observed 415class NeedRenderSize { // properties usually used together can be divided into the same new divided class 416 public width: number = 336; 417 public height: number = 178; 418} 419@Observed 420class NeedRenderPos { // properties usually used together can be divided into the same new divided class 421 public posX: number = 10; 422 public posY: number = 50; 423} 424@Observed 425class NeedRenderBorderRadius { // properties that may be used in different places can be divided into the same new divided class 426 public borderRadius: number = 24; 427} 428@Observed 429class NeedRenderFontSize { // properties that may be used in different places can be divided into the same new divided class 430 public fontSize: number = 20; 431} 432@Observed 433class NeedRenderTranslate { // properties usually used together can be divided into the same new divided class 434 public translateX: number = 0; 435 public translateY: number = 0; 436} 437@Observed 438class UIStyle { 439 // define new variable instead of using old one 440 needRenderTranslate: NeedRenderTranslate = new NeedRenderTranslate(); 441 needRenderFontSize: NeedRenderFontSize = new NeedRenderFontSize(); 442 needRenderBorderRadius: NeedRenderBorderRadius = new NeedRenderBorderRadius(); 443 needRenderPos: NeedRenderPos = new NeedRenderPos(); 444 needRenderSize: NeedRenderSize = new NeedRenderSize(); 445 needRenderAlpha: NeedRenderAlpha = new NeedRenderAlpha(); 446 needRenderScale: NeedRenderScale = new NeedRenderScale(); 447 needRenderImage: NeedRenderImage = new NeedRenderImage(); 448} 449@Component 450struct SpecialImage { 451 @ObjectLink uiStyle : UIStyle; 452 @ObjectLink needRenderImage: NeedRenderImage // receive the new class from its parent component 453 private isRenderSpecialImage() : number { // function to show whether the component is rendered 454 console.log("SpecialImage is rendered"); 455 return 1; 456 } 457 build() { 458 Image($r('app.media.icon')) 459 .width(this.needRenderImage.imageWidth) // !! use this.needRenderImage.xxx rather than this.uiStyle.needRenderImage.xxx !! 460 .height(this.needRenderImage.imageHeight) 461 .margin({top:20}) 462 .translate({ 463 x: this.needRenderImage.translateImageX, 464 y: this.needRenderImage.translateImageY 465 }) 466 .opacity(this.isRenderSpecialImage()) // if the Image is rendered, it will call the function 467 } 468} 469@Component 470struct CompA { 471 @ObjectLink uiStyle: UIStyle; 472 @ObjectLink needRenderTranslate: NeedRenderTranslate; // receive the new class from its parent component 473 @ObjectLink needRenderFontSize: NeedRenderFontSize; 474 @ObjectLink needRenderBorderRadius: NeedRenderBorderRadius; 475 @ObjectLink needRenderPos: NeedRenderPos; 476 @ObjectLink needRenderSize: NeedRenderSize; 477 @ObjectLink needRenderAlpha: NeedRenderAlpha; 478 @ObjectLink needRenderScale: NeedRenderScale; 479 // the following functions are used to show whether the component is called to be rendered 480 private isRenderColumn() : number { 481 console.log("Column is rendered"); 482 return 1; 483 } 484 private isRenderStack() : number { 485 console.log("Stack is rendered"); 486 return 1; 487 } 488 private isRenderImage() : number { 489 console.log("Image is rendered"); 490 return 1; 491 } 492 private isRenderText() : number { 493 console.log("Text is rendered"); 494 return 1; 495 } 496 build() { 497 Column() { 498 // when you compile this code in API9, IDE may tell you that 499 // "Assigning the '@ObjectLink' decorated attribute 'uiStyle' to the '@ObjectLink' decorated attribute 'uiStyle' is not allowed. <etsLint>" 500 // "Assigning the '@ObjectLink' decorated attribute 'uiStyle' to the '@ObjectLink' decorated attribute 'needRenderImage' is not allowed. <etsLint>" 501 // But you can still run the code by Previewer 502 SpecialImage({ 503 uiStyle: this.uiStyle, 504 needRenderImage: this.uiStyle.needRenderImage //send it to its child 505 }) 506 Stack() { 507 Column() { 508 Image($r('app.media.icon')) 509 .opacity(this.needRenderAlpha.alpha) 510 .scale({ 511 x: this.needRenderScale.scaleX, // use this.needRenderXxx.xxx rather than this.uiStyle.needRenderXxx.xxx 512 y: this.needRenderScale.scaleY 513 }) 514 .padding(this.isRenderImage()) 515 .width(300) 516 .height(300) 517 } 518 .width('100%') 519 .position({ y: -80 }) 520 521 Stack() { 522 Text("Hello World") 523 .fontColor("#182431") 524 .fontWeight(FontWeight.Medium) 525 .fontSize(this.needRenderFontSize.fontSize) 526 .opacity(this.isRenderText()) 527 .margin({ top: 12 }) 528 } 529 .opacity(this.isRenderStack()) 530 .position({ 531 x: this.needRenderPos.posX, 532 y: this.needRenderPos.posY 533 }) 534 .width('100%') 535 .height('100%') 536 } 537 .margin({ top: 50 }) 538 .borderRadius(this.needRenderBorderRadius.borderRadius) 539 .opacity(this.isRenderStack()) 540 .backgroundColor("#FFFFFF") 541 .width(this.needRenderSize.width) 542 .height(this.needRenderSize.height) 543 .translate({ 544 x: this.needRenderTranslate.translateX, 545 y: this.needRenderTranslate.translateY 546 }) 547 548 Column() { 549 Button("Move") 550 .width(312) 551 .fontSize(20) 552 .backgroundColor("#FF007DFF") 553 .margin({ bottom: 10 }) 554 .onClick(() => { 555 animateTo({ 556 duration: 500 557 }, () => { 558 this.needRenderTranslate.translateY = (this.needRenderTranslate.translateY + 180) % 250; 559 }) 560 }) 561 Button("Scale") 562 .borderRadius(20) 563 .backgroundColor("#FF007DFF") 564 .fontSize(20) 565 .width(312) 566 .margin({ bottom: 10 }) 567 .onClick(() => { 568 this.needRenderScale.scaleX = (this.needRenderScale.scaleX + 0.6) % 0.8; 569 }) 570 Button("Change Image") 571 .borderRadius(20) 572 .backgroundColor("#FF007DFF") 573 .fontSize(20) 574 .width(312) 575 .onClick(() => { // in the parent component, still use this.uiStyle.needRenderXxx.xxx to change the properties 576 this.uiStyle.needRenderImage.imageWidth = (this.uiStyle.needRenderImage.imageWidth + 30) % 160; 577 this.uiStyle.needRenderImage.imageHeight = (this.uiStyle.needRenderImage.imageHeight + 30) % 160; 578 }) 579 } 580 .position({ 581 y: 616 582 }) 583 .height('100%') 584 .width('100%') 585 } 586 .opacity(this.isRenderColumn()) 587 .width('100%') 588 .height('100%') 589 } 590} 591@Entry 592@Component 593struct Page { 594 @State uiStyle: UIStyle = new UIStyle(); 595 build() { 596 Stack() { 597 CompA({ 598 uiStyle: this.uiStyle, 599 needRenderTranslate: this.uiStyle.needRenderTranslate, //send all the new class child need 600 needRenderFontSize: this.uiStyle.needRenderFontSize, 601 needRenderBorderRadius: this.uiStyle.needRenderBorderRadius, 602 needRenderPos: this.uiStyle.needRenderPos, 603 needRenderSize: this.uiStyle.needRenderSize, 604 needRenderAlpha: this.uiStyle.needRenderAlpha, 605 needRenderScale: this.uiStyle.needRenderScale 606 }) 607 } 608 .backgroundColor("#F1F3F5") 609 } 610} 611``` 612 613上文代码运行图示如下: 614 615 616 617在上面的示例中将原先大类中的十五个属性拆成了八个小类,并且在组件的属性绑定中也进行了相应的适配。属性拆分遵循以下几点原则: 618 619- 只作用在同一个组件上的多个属性可以被拆分进同一个新类,即示例中的NeedRenderImage。适用于组件经常被不关联的属性改变而引起刷新的场景,这个时候就要考虑拆分属性,或者重新考虑ViewModel设计是否合理。 620- 经常被同时使用的属性可以被拆分进同一个新类,即示例中的NeedRenderScale、NeedRenderTranslate、NeedRenderPos、NeedRenderSize。适用于属性经常成对出现,或者被作用在同一个样式上的情况,例如.translate、.position、.scale等(这些样式通常会接收一个对象作为参数)。 621- 可能被用在多个组件上或相对较独立的属性应该被单独拆分进一个新类,即示例中的NeedRenderAlpha,NeedRenderBorderRadius、NeedRenderFontSize。适用于一个属性作用在多个组件上或者与其他属性没有联系的情况,例如.opacity、.borderRadius等(这些样式通常相对独立)。 622 623在对属性进行拆分后,对所有使用属性对组件进行绑定的时候,需要使用以下格式: 624 625```ts 626.property(this.needRenderXxx.xxx) 627 628// sample 629Text("some text") 630.width(this.needRenderSize.width) 631.height(this.needRenderSize.height) 632.opacity(this.needRenderAlpha.alpha) 633``` 634 635在父组件改变属性的值时,可以通过外层的父类去修改,即: 636 637```ts 638// in parent Component 639this.parent.needRenderXxx.xxx = x; 640 641//example 642this.uiStyle.needRenderImage.imageWidth = (this.uiStyle.needRenderImage.imageWidth + 20) % 60; 643``` 644 645在子组件本身改变属性的值时,推荐直接通过新类去修改,即: 646 647```ts 648// in child Component 649this.needRenderXxx.xxx = x; 650 651//example 652this.needRenderScale.scaleX = (this.needRenderScale.scaleX + 0.6) % 1 653``` 654 655属性拆分应当重点考虑变化较为频繁的属性,来提高应用运行的性能。 656 657如果想要在父组件中使用拆分后的属性,推荐新定义一个@State修饰的状态变量配合使用。 658 659```ts 660@Observed 661class NeedRenderProperty { 662 public property: number = 1; 663}; 664@Observed 665class SomeClass { 666 needRenderProperty: NeedRenderProperty = new NeedRenderProperty(); 667} 668@Entry 669@Component 670struct Page { 671 @State someClass: SomeClass = new SomeClass(); 672 @State needRenderProperty: NeedRenderProperty = this.someClass.needRenderProperty 673 build() { 674 Row() { 675 Column() { 676 Text("property value: " + this.needRenderProperty.property) 677 .fontSize(30) 678 .margin({ bottom: 20 }) 679 Button("Change property") 680 .onClick(() => { 681 this.needRenderProperty.property++; 682 }) 683 } 684 .width('100%') 685 } 686 .width('100%') 687 .height('100%') 688 } 689} 690``` 691 692