• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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![precisely-control-render-scope-01.gif](figures/precisely-control-render-scope-01.gif)
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![precisely-control-render-scope-02.gif](figures/precisely-control-render-scope-02.gif)
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![precisely-control-render-scope-03.gif](figures/precisely-control-render-scope-03.gif)
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![precisely-control-render-scope-04.gif](figures/precisely-control-render-scope-04.gif)
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![precisely-control-render-scope-05.gif](figures/precisely-control-render-scope-05.gif)
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