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