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![precisely-control-render-scope-01.gif](figures/precisely-control-render-scope-01.gif)
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![precisely-control-render-scope-02.gif](figures/precisely-control-render-scope-02.gif)
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![precisely-control-render-scope-03.gif](figures/precisely-control-render-scope-03.gif)
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![precisely-control-render-scope-04.gif](figures/precisely-control-render-scope-04.gif)
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![precisely-control-render-scope-05.gif](figures/precisely-control-render-scope-05.gif)
618
619可以使用SmartPerf Host工具分别抓取优化前后点击“move”按钮时的trace数据,来查看属性拆分的性能收益。
620
621优化前点击move按钮的脏节点更新耗时如下图:
622
623![precisely-control-render-scope-dirty-node-trace-01](figures/precisely-control-render-scope-dirty-node-trace-01.PNG)
624
625优化后点击move按钮的脏节点更新耗时如下图:
626
627![precisely-control-render-scope-dirty-node-trace-02](figures/precisely-control-render-scope-dirty-node-trace-02.PNG)
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![redundant_refresh](./figures/redundant_refresh.gif)
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![precise_refresh.gif](./figures/precise_refresh.gif)
853
854【效果对比】
855
856使用SmartPerf Host工具分别抓取正反例中切换选中项的trace数据,通过点击组件2的脏节点更新耗时分析二者的性能差异。
857
858反例点击组件2的脏节点更新耗时如下图:
859
860![precisely-control-render-scope-dirty-node-trace-03](figures/precisely-control-render-scope-dirty-node-trace-03.PNG)
861
862正例点击组件2的脏节点更新耗时如下图:
863
864![precisely-control-render-scope-dirty-node-trace-04](figures/precisely-control-render-scope-dirty-node-trace-04.PNG)
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```