• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# 状态管理优秀实践
2
3
4为了帮助应用程序开发人员提高其应用程序质量,特别是在高效的状态管理方面。本章节面向开发者提供了多个在开发ArkUI应用中常见场景和易错问题,并给出了对应的解决方案。此外,还提供了同一场景下,推荐用法和不推荐用法的对比和解释说明,更直观地展示两者区别,从而帮助开发者学习如果正确地在应用开发中使用状态变量,进行高性能开发。
5
6
7## 基础示例
8
9下面的例子是关于\@Prop,\@Link,\@ObjectLink的初始化规则的,在学习下面这个例子前,我们首先需要了解:
10
11- \@Prop:可以被父组件的\@State初始化,或者\@State是复杂类型Object和class时的属性,或者是数组时的数组项。
12
13- \@ObjectLink:初始化规则和\@Prop相同,但需要被\@Observed装饰class的实例初始化。
14
15- \@Link:必须和\@State或其他数据源类型完全相同。
16
17
18### 不推荐用法
19
20
21
22```ts
23@Observed
24class ClassA {
25  public c: number = 0;
26
27  constructor(c: number) {
28    this.c = c;
29  }
30}
31
32@Component
33struct LinkChild {
34  @Link testNum: number;
35
36  build() {
37    Text(`LinkChild testNum ${this.testNum}`)
38  }
39}
40
41
42@Component
43struct PropChild2 {
44  @Prop testNum: ClassA = new ClassA(0);
45
46  build() {
47    Text(`PropChild2 testNum ${this.testNum.c}`)
48      .onClick(() => {
49        this.testNum.c += 1;
50      })
51  }
52}
53
54@Component
55struct PropChild3 {
56  @Prop testNum: ClassA = new ClassA(0);
57
58  build() {
59    Text(`PropChild3 testNum ${this.testNum.c}`)
60  }
61}
62
63@Component
64struct ObjectLinkChild {
65  @ObjectLink testNum: ClassA;
66
67  build() {
68    Text(`ObjectLinkChild testNum ${this.testNum.c}`)
69      .onClick(() => {
70        // 问题4:ObjectLink不能被赋值
71        this.testNum = new ClassA(47);
72      })
73  }
74}
75
76@Entry
77@Component
78struct Parent {
79  @State testNum: ClassA[] = [new ClassA(1)];
80
81  build() {
82    Column() {
83      Text(`Parent testNum ${this.testNum.c}`)
84        .onClick(() => {
85          this.testNum[0].c += 1;
86        })
87      // 问题1:@Link装饰的变量需要和数据源@State类型一致
88      LinkChild({ testNum: this.testNum.c })
89
90      // 问题2:@Prop本地没有初始化,也没有从父组件初始化
91      PropChild2()
92
93      // 问题3:PropChild3没有改变@Prop testNum: ClassA的值,所以这时最优的选择是使用@ObjectLink
94      PropChild3({ testNum: this.testNum[0] })
95
96      ObjectLinkChild({ testNum: this.testNum[0] })
97    }
98  }
99}
100```
101
102
103上面的例子有以下几个错误:
104
105
1061. \@Component LinkChild:\@Link testNum: number从父组件的LinkChild({testNum:this.testNum.c})。\@Link的数据源必须是装饰器装饰的状态变量,简而言之,\@Link装饰的数据必须和数据源类型相同,比如\@Link: T和\@State : T。所以,这里应该改为\@Link testNum: ClassA,从父组件初始化的方式为LinkChild({testNum: $testNum})。
107
1082. \@Component PropChild2:\@Prop可以本地初始化,也可以从父组件初始化,但是必须初始化,对于\@Prop testNum: ClassA没有本地初始化,所以必须从父组件初始化PropChild1({testNum: this.testNum})。
109
1103. \@Component PropChild3:没有改变\@Prop testNum: ClassA的值,所以这时较优的选择是使用\@ObjectLink,因为\@Prop是会深拷贝数据,具有拷贝的性能开销,所以这个时候\@ObjectLink是比\@Link和\@Prop更优的选择。
111
1124. 点击ObjectLinkChild给\@ObjectLink装饰的变量赋值:this.testNum = new ClassA(47); 也是不允许的,对于实现双向数据同步的\@ObjectLink,赋值相当于要更新父组件中的数组项或者class的属性,这个对于 TypeScript/JavaScript是不能实现的。框架对于这种行为会发生运行时报错。
113
1145. 如果是非嵌套场景,比如Parent里声明的变量为 \@State testNum: ClassA = new ClassA(1),ClassA就不需要被\@Observed修饰,因为\@State已经具备了观察第一层变化的能力,不需要再使用\@Observed来加一层代理。
115
116
117### 推荐用法
118
119
120
121```ts
122@Observed
123class ClassA {
124  public c: number = 0;
125
126  constructor(c: number) {
127    this.c = c;
128  }
129}
130
131@Component
132struct LinkChild {
133 @Link testNum: ClassA;
134
135 build() {
136   Text(`LinkChild testNum ${this.testNum?.c}`)
137 }
138}
139
140@Component
141struct PropChild1 {
142 @Prop testNum: ClassA = new ClassA(1);
143
144 build() {
145   Text(`PropChild1 testNum ${this.testNum?.c}`)
146     .onClick(() => {
147       this.testNum = new ClassA(48);
148     })
149 }
150}
151
152@Component
153struct ObjectLinkChild {
154  @ObjectLink testNum: ClassA;
155
156  build() {
157    Text(`ObjectLinkChild testNum ${this.testNum.c}`)
158      // @ObjectLink装饰的变量可以更新属性
159      .onClick(() => {
160        this.testNum.c += 1;
161      })
162  }
163}
164
165@Entry
166@Component
167struct Parent {
168  @State testNum: ClassA[] = [new ClassA(1)];
169
170  build() {
171    Column() {
172      Text(`Parent testNum ${this.testNum.c}`)
173        .onClick(() => {
174          this.testNum[0].c += 1;
175        })
176      // @Link装饰的变量需要和数据源@State类型一致
177      LinkChild({ testNum: this.testNum[0] })
178
179      // @Prop本地有初始化,不需要再从父组件初始化
180      PropChild1()
181
182      // 当子组件不需要发生本地改变时,优先使用@ObjectLink,因为@Prop是会深拷贝数据,具有拷贝的性能开销,所以这个时候@ObjectLink是比@Link和@Prop更优的选择
183      ObjectLinkChild({ testNum: this.testNum[0] })
184    }
185  }
186}
187```
188
189
190
191## 基础嵌套对象属性更改失效
192
193在应用开发中,有很多嵌套对象场景,例如,开发者更新了某个属性,但UI没有进行对应的更新。
194
195每个装饰器都有自己可以观察的能力,并不是所有的改变都可以被观察到,只有可以被观察到的变化才会进行UI更新。\@Observed装饰器可以观察到嵌套对象的属性变化,其他装饰器仅能观察到第二层的变化。
196
197
198### 不推荐用法
199
200下面的例子中,一些UI组件并不会更新。
201
202
203```ts
204class ClassA {
205  a: number;
206
207  constructor(a: number) {
208    this.a = a;
209  }
210
211  getA(): number {
212    return this.a;
213  }
214
215  setA(a: number): void {
216    this.a = a;
217  }
218}
219
220class ClassC {
221  c: number;
222
223  constructor(c: number) {
224    this.c = c;
225  }
226
227  getC(): number {
228    return this.c;
229  }
230
231  setC(c: number): void {
232    this.c = c;
233  }
234}
235
236class ClassB extends ClassA {
237  b: number = 47;
238  c: ClassC;
239
240  constructor(a: number, b: number, c: number) {
241    super(a);
242    this.b = b;
243    this.c = new ClassC(c);
244  }
245
246  getB(): number {
247    return this.b;
248  }
249
250  setB(b: number): void {
251    this.b = b;
252  }
253
254  getC(): number {
255    return this.c.getC();
256  }
257
258  setC(c: number): void {
259    return this.c.setC(c);
260  }
261}
262
263
264@Entry
265@Component
266struct MyView {
267  @State b: ClassB = new ClassB(10, 20, 30);
268
269  build() {
270    Column({ space: 10 }) {
271      Text(`a: ${this.b.a}`)
272      Button("Change ClassA.a")
273        .onClick(() => {
274          this.b.a += 1;
275        })
276
277      Text(`b: ${this.b.b}`)
278      Button("Change ClassB.b")
279        .onClick(() => {
280          this.b.b += 1;
281        })
282
283      Text(`c: ${this.b.c.c}`)
284      Button("Change ClassB.ClassC.c")
285        .onClick(() => {
286          // 点击时上面的Text组件不会刷新
287          this.b.c.c += 1;
288        })
289    }
290  }
291}
292```
293
294- 最后一个Text组件Text('c: ${this.b.c.c}'),当点击该组件时UI不会刷新。 因为,\@State b : ClassB 只能观察到this.b属性的变化,比如this.b.a, this.b.bthis.b.c的变化,但是无法观察嵌套在属性中的属性,即this.b.c.c(属性c是内嵌在b中的对象classC的属性)。
295
296- 为了观察到嵌套与内部的ClassC的属性,需要做如下改变:
297  - 构造一个子组件,用于单独渲染ClassC的实例。 该子组件可以使用\@ObjectLink c : ClassC或\@Prop c : ClassC。通常会使用\@ObjectLink,除非子组件需要对其ClassC对象进行本地修改。
298  - 嵌套的ClassC必须用\@Observed修饰。当在ClassB中创建ClassC对象时(本示例中的ClassB(10, 20, 30)),它将被包装在ES6代理中,当ClassC属性更改时(this.b.c.c += 1),该代码将修改通知到\@ObjectLink变量。
299
300
301### 推荐用法
302
303以下示例使用\@Observed/\@ObjectLink来观察嵌套对象的属性更改。
304
305
306```ts
307class ClassA {
308  a: number;
309  constructor(a: number) {
310    this.a = a;
311  }
312  getA() : number {
313    return this.a; }
314  setA( a: number ) : void {
315    this.a = a; }
316}
317
318@Observed
319class ClassC {
320  c: number;
321  constructor(c: number) {
322    this.c = c;
323  }
324  getC() : number {
325    return this.c; }
326  setC(c : number) : void {
327    this.c = c; }
328}
329
330class ClassB extends ClassA {
331  b: number = 47;
332  c: ClassC;
333
334  constructor(a: number, b: number, c: number) {
335    super(a);
336    this.b = b;
337    this.c = new ClassC(c);
338  }
339
340  getB() : number {
341    return this.b; }
342  setB(b : number) : void {
343    this.b = b; }
344  getC() : number {
345    return this.c.getC(); }
346  setC(c : number) : void {
347    return this.c.setC(c); }
348}
349
350@Component
351struct ViewClassC {
352
353    @ObjectLink c : ClassC;
354    build() {
355        Column({space:10}) {
356            Text(`c: ${this.c.getC()}`)
357            Button("Change C")
358                .onClick(() => {
359                    this.c.setC(this.c.getC()+1);
360                })
361        }
362    }
363}
364
365@Entry
366@Component
367struct MyView {
368    @State b : ClassB = new ClassB(10, 20, 30);
369
370    build() {
371        Column({space:10}) {
372            Text(`a: ${this.b.a}`)
373             Button("Change ClassA.a")
374            .onClick(() => {
375                this.b.a +=1;
376            })
377
378            Text(`b: ${this.b.b}`)
379            Button("Change ClassB.b")
380            .onClick(() => {
381                this.b.b += 1;
382            })
383
384            ViewClassC({c: this.b.c})   // Text(`c: ${this.b.c.c}`)的替代写法
385            Button("Change ClassB.ClassC.c")
386            .onClick(() => {
387                this.b.c.c += 1;
388            })
389        }
390     }
391}
392```
393
394
395
396## 复杂嵌套对象属性更改失效
397
398
399### 不推荐用法
400
401以下示例创建了一个带有\@ObjectLink装饰变量的子组件,用于渲染一个含有嵌套属性的ParentCounter,用\@Observed装饰嵌套在ParentCounter中的SubCounter。
402
403
404```ts
405let nextId = 1;
406@Observed
407class SubCounter {
408  counter: number;
409  constructor(c: number) {
410    this.counter = c;
411  }
412}
413@Observed
414class ParentCounter {
415  id: number;
416  counter: number;
417  subCounter: SubCounter;
418  incrCounter() {
419    this.counter++;
420  }
421  incrSubCounter(c: number) {
422    this.subCounter.counter += c;
423  }
424  setSubCounter(c: number): void {
425    this.subCounter.counter = c;
426  }
427  constructor(c: number) {
428    this.id = nextId++;
429    this.counter = c;
430    this.subCounter = new SubCounter(c);
431  }
432}
433@Component
434struct CounterComp {
435  @ObjectLink value: ParentCounter;
436  build() {
437    Column({ space: 10 }) {
438      Text(`${this.value.counter}`)
439        .fontSize(25)
440        .onClick(() => {
441          this.value.incrCounter();
442        })
443      Text(`${this.value.subCounter.counter}`)
444        .onClick(() => {
445          this.value.incrSubCounter(1);
446        })
447      Divider().height(2)
448    }
449  }
450}
451@Entry
452@Component
453struct ParentComp {
454  @State counter: ParentCounter[] = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)];
455  build() {
456    Row() {
457      Column() {
458        CounterComp({ value: this.counter[0] })
459        CounterComp({ value: this.counter[1] })
460        CounterComp({ value: this.counter[2] })
461        Divider().height(5)
462        ForEach(this.counter,
463          (item: ParentCounter) => {
464            CounterComp({ value: item })
465          },
466          (item: ParentCounter) => item.id.toString()
467        )
468        Divider().height(5)
469        // 第一个点击事件
470        Text('Parent: incr counter[0].counter')
471          .fontSize(20).height(50)
472          .onClick(() => {
473            this.counter[0].incrCounter();
474            // 每次触发时自增10
475            this.counter[0].incrSubCounter(10);
476          })
477        // 第二个点击事件
478        Text('Parent: set.counter to 10')
479          .fontSize(20).height(50)
480          .onClick(() => {
481            // 无法将value设置为10,UI不会刷新
482            this.counter[0].setSubCounter(10);
483          })
484        Text('Parent: reset entire counter')
485          .fontSize(20).height(50)
486          .onClick(() => {
487            this.counter = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)];
488          })
489      }
490    }
491  }
492}
493```
494
495对于Text('Parent: incr counter[0].counter')的onClick事件,this.counter[0].incrSubCounter(10)调用incrSubCounter方法使SubCounter的counter值增加10,UI同步刷新。
496
497但是,在Text('Parent: set.counter to 10')的onClick中调用this.counter[0].setSubCounter(10),SubCounter的counter值却无法重置为10。
498
499incrSubCounter和setSubCounter都是同一个SubCounter的函数。在第一个点击处理时调用incrSubCounter可以正确更新UI,而第二个点击处理调用setSubCounter时却没有更新UI。实际上incrSubCounter和setSubCounter两个函数都不能触发Text('${this.value.subCounter.counter}')的更新,因为\@ObjectLink value : ParentCounter仅能观察其代理ParentCounter的属性,对于this.value.subCounter.counter是SubCounter的属性,无法观察到嵌套类的属性。
500
501但是,第一个click事件调用this.counter[0].incrCounter()将CounterComp自定义组件中\@ObjectLink value: ParentCounter标记为已更改。此时触发Text('${this.value.subCounter.counter}')的更新。 如果在第一个点击事件中删除this.counter[0].incrCounter(),也无法更新UI。
502
503
504### 推荐用法
505
506对于上述问题,为了直接观察SubCounter中的属性,以便this.counter[0].setSubCounter(10)操作有效,可以利用下面的方法:
507
508
509```ts
510@ObjectLink value:ParentCounter = new ParentCounter(0);
511@ObjectLink subValue:SubCounter = new SubCounter(0);
512```
513
514该方法使得\@ObjectLink分别代理了ParentCounter和SubCounter的属性,这样对于这两个类的属性的变化都可以观察到,即都会对UI视图进行刷新。即使删除了上面所说的this.counter[0].incrCounter(),UI也会进行正确的刷新。
515
516该方法可用于实现“两个层级”的观察,即外部对象和内部嵌套对象的观察。但是该方法只能用于\@ObjectLink装饰器,无法作用于\@Prop(\@Prop通过深拷贝传入对象)。详情参考@Prop与@ObjectLink的差异。
517
518
519```ts
520let nextId = 1;
521@Observed
522class SubCounter {
523  counter: number;
524  constructor(c: number) {
525    this.counter = c;
526  }
527}
528@Observed
529class ParentCounter {
530  id: number;
531  counter: number;
532  subCounter: SubCounter;
533  incrCounter() {
534    this.counter++;
535  }
536  incrSubCounter(c: number) {
537    this.subCounter.counter += c;
538  }
539  setSubCounter(c: number): void {
540    this.subCounter.counter = c;
541  }
542  constructor(c: number) {
543    this.id = nextId++;
544    this.counter = c;
545    this.subCounter = new SubCounter(c);
546  }
547}
548@Component
549struct CounterComp {
550  @ObjectLink value: ParentCounter;
551  @ObjectLink subValue: SubCounter;
552  build() {
553    Column({ space: 10 }) {
554      Text(`${this.value.counter}`)
555        .fontSize(25)
556        .onClick(() => {
557          this.value.incrCounter();
558        })
559      Text(`${this.subValue.counter}`)
560        .onClick(() => {
561          this.subValue.counter += 1;
562        })
563      Divider().height(2)
564    }
565  }
566}
567@Entry
568@Component
569struct ParentComp {
570  @State counter: ParentCounter[] = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)];
571  build() {
572    Row() {
573      Column() {
574        CounterComp({ value: this.counter[0], subValue: this.counter[0].subCounter })
575        CounterComp({ value: this.counter[1], subValue: this.counter[1].subCounter })
576        CounterComp({ value: this.counter[2], subValue: this.counter[2].subCounter })
577        Divider().height(5)
578        ForEach(this.counter,
579          (item: ParentCounter) => {
580            CounterComp({ value: item, subValue: item.subCounter })
581          },
582          (item: ParentCounter) => item.id.toString()
583        )
584        Divider().height(5)
585        Text('Parent: reset entire counter')
586          .fontSize(20).height(50)
587          .onClick(() => {
588            this.counter = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)];
589          })
590        Text('Parent: incr counter[0].counter')
591          .fontSize(20).height(50)
592          .onClick(() => {
593            this.counter[0].incrCounter();
594            this.counter[0].incrSubCounter(10);
595          })
596        Text('Parent: set.counter to 10')
597          .fontSize(20).height(50)
598          .onClick(() => {
599            this.counter[0].setSubCounter(10);
600          })
601      }
602    }
603  }
604}
605```
606
607
608## \@Prop与\@ObjectLink的差异
609
610在下面的示例代码中,\@ObjectLink修饰的变量是对数据源的引用,即在this.value.subValuethis.subValue都是同一个对象的不同引用,所以在点击CounterComp的click handler,改变this.value.subCounter.counterthis.subValue.counter也会改变,对应的组件Text(`this.subValue.counter: ${this.subValue.counter}`)会刷新。
611
612
613```ts
614let nextId = 1;
615
616@Observed
617class SubCounter {
618  counter: number;
619  constructor(c: number) {
620    this.counter = c;
621  }
622}
623
624@Observed
625class ParentCounter {
626  id: number;
627  counter: number;
628  subCounter: SubCounter;
629  incrCounter() {
630    this.counter++;
631  }
632  incrSubCounter(c: number) {
633    this.subCounter.counter += c;
634  }
635  setSubCounter(c: number): void {
636    this.subCounter.counter = c;
637  }
638  constructor(c: number) {
639    this.id = nextId++;
640    this.counter = c;
641    this.subCounter = new SubCounter(c);
642  }
643}
644
645@Component
646struct CounterComp {
647  @ObjectLink value: ParentCounter;
648  @ObjectLink subValue: SubCounter;
649  build() {
650    Column({ space: 10 }) {
651      Text(`this.subValue.counter: ${this.subValue.counter}`)
652        .fontSize(30)
653      Text(`this.value.counter:increase 7 `)
654        .fontSize(30)
655        .onClick(() => {
656          // click handler, Text(`this.subValue.counter: ${this.subValue.counter}`) will update
657          this.value.incrSubCounter(7);
658        })
659      Divider().height(2)
660    }
661  }
662}
663
664@Entry
665@Component
666struct ParentComp {
667  @State counter: ParentCounter[] = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)];
668  build() {
669    Row() {
670      Column() {
671        CounterComp({ value: this.counter[0], subValue: this.counter[0].subCounter })
672        CounterComp({ value: this.counter[1], subValue: this.counter[1].subCounter })
673        CounterComp({ value: this.counter[2], subValue: this.counter[2].subCounter })
674        Divider().height(5)
675        ForEach(this.counter,
676          (item: ParentCounter) => {
677            CounterComp({ value: item, subValue: item.subCounter })
678          },
679          (item: ParentCounter) => item.id.toString()
680        )
681        Divider().height(5)
682        Text('Parent: reset entire counter')
683          .fontSize(20).height(50)
684          .onClick(() => {
685            this.counter = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)];
686          })
687        Text('Parent: incr counter[0].counter')
688          .fontSize(20).height(50)
689          .onClick(() => {
690            this.counter[0].incrCounter();
691            this.counter[0].incrSubCounter(10);
692          })
693        Text('Parent: set.counter to 10')
694          .fontSize(20).height(50)
695          .onClick(() => {
696            this.counter[0].setSubCounter(10);
697          })
698      }
699    }
700  }
701}
702```
703
704\@ObjectLink图示如下:
705
706![zh-cn_image_0000001651665921](figures/zh-cn_image_0000001651665921.png)
707
708
709### 不推荐用法
710
711如果用\@Prop替代\@ObjectLink。点击第一个click handler,UI刷新正常。但是点击第二个onClick事件,\@Prop 对变量做了一个本地拷贝,CounterComp的第一个Text并不会刷新。
712
713  this.value.subCounterthis.subValue并不是同一个对象。所以this.value.subCounter的改变,并没有改变this.subValue的拷贝对象,Text(`this.subValue.counter: ${this.subValue.counter}`)不会刷新。
714
715```ts
716@Component
717struct CounterComp {
718  @Prop value: ParentCounter = new ParentCounter(0);
719  @Prop subValue: SubCounter = new SubCounter(0);
720  build() {
721    Column({ space: 10 }) {
722      Text(`this.subValue.counter: ${this.subValue.counter}`)
723        .fontSize(20)
724        .onClick(() => {
725          // 1st click handler
726          this.subValue.counter += 7;
727        })
728      Text(`this.value.counter:increase 7 `)
729        .fontSize(20)
730        .onClick(() => {
731          // 2nd click handler
732          this.value.incrSubCounter(7);
733        })
734      Divider().height(2)
735    }
736  }
737}
738```
739
740\@Prop拷贝的关系图示如下:
741
742![zh-cn_image_0000001602146116](figures/zh-cn_image_0000001602146116.png)
743
744
745### 推荐用法
746
747可以通过从ParentComp到CounterComp仅拷贝一份\@Prop value: ParentCounter,同时必须避免再多拷贝一份SubCounter。
748
749- 在CounterComp组件中只使用一个\@Prop counter:Counter。
750
751- 添加另一个子组件SubCounterComp,其中包含\@ObjectLink subCounter: SubCounter。此\@ObjectLink可确保观察到SubCounter对象属性更改,并且UI更新正常。
752
753- \@ObjectLink subCounter: SubCounter与CounterComp中的\@Prop counter:Counter的this.counter.subCounter共享相同的SubCounter对象。
754
755
756```ts
757let nextId = 1;
758
759@Observed
760class SubCounter {
761  counter: number;
762  constructor(c: number) {
763    this.counter = c;
764  }
765}
766
767@Observed
768class ParentCounter {
769  id: number;
770  counter: number;
771  subCounter: SubCounter;
772  incrCounter() {
773    this.counter++;
774  }
775  incrSubCounter(c: number) {
776    this.subCounter.counter += c;
777  }
778  setSubCounter(c: number): void {
779    this.subCounter.counter = c;
780  }
781  constructor(c: number) {
782    this.id = nextId++;
783    this.counter = c;
784    this.subCounter = new SubCounter(c);
785  }
786}
787
788@Component
789struct SubCounterComp {
790  @ObjectLink subValue: SubCounter;
791  build() {
792    Text(`SubCounterComp: this.subValue.counter: ${this.subValue.counter}`)
793      .onClick(() => {
794        // 2nd click handler
795        this.subValue.counter = 7;
796      })
797  }
798}
799@Component
800struct CounterComp {
801  @ObjectLink value: ParentCounter;
802  build() {
803    Column({ space: 10 }) {
804      Text(`this.value.incrCounter(): this.value.counter: ${this.value.counter}`)
805        .fontSize(20)
806        .onClick(() => {
807          // 1st click handler
808          this.value.incrCounter();
809        })
810      SubCounterComp({ subValue: this.value.subCounter })
811      Text(`this.value.incrSubCounter()`)
812        .onClick(() => {
813          // 3rd click handler
814          this.value.incrSubCounter(77);
815        })
816      Divider().height(2)
817    }
818  }
819}
820@Entry
821@Component
822struct ParentComp {
823  @State counter: ParentCounter[] = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)];
824  build() {
825    Row() {
826      Column() {
827        CounterComp({ value: this.counter[0] })
828        CounterComp({ value: this.counter[1] })
829        CounterComp({ value: this.counter[2] })
830        Divider().height(5)
831        ForEach(this.counter,
832          (item: ParentCounter) => {
833            CounterComp({ value: item })
834          },
835          (item: ParentCounter) => item.id.toString()
836        )
837        Divider().height(5)
838        Text('Parent: reset entire counter')
839          .fontSize(20).height(50)
840          .onClick(() => {
841            this.counter = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)];
842          })
843        Text('Parent: incr counter[0].counter')
844          .fontSize(20).height(50)
845          .onClick(() => {
846            this.counter[0].incrCounter();
847            this.counter[0].incrSubCounter(10);
848          })
849        Text('Parent: set.counter to 10')
850          .fontSize(20).height(50)
851          .onClick(() => {
852            this.counter[0].setSubCounter(10);
853          })
854      }
855    }
856  }
857}
858```
859
860
861拷贝关系图示如下:
862
863
864![zh-cn_image_0000001653949465](figures/zh-cn_image_0000001653949465.png)
865
866
867## 应用在渲染期间禁止改变状态变量
868
869在学习本示例之前,我们要先明确一个概念,在ArkUI状态管理中,状态驱动UI更新。
870
871![zh-cn_image_0000001651365257](figures/zh-cn_image_0000001651365257.png)
872
873所以,不能在自定义组件的build()或\@Builder方法里直接改变状态变量,这可能会造成循环渲染的风险,下面以build()方法举例示意。
874
875
876### 不推荐用法
877
878在下面的示例中,Text('${this.count++}')在build渲染方法里直接改变了状态变量。
879
880
881```ts
882@Entry
883@Component
884struct CompA {
885  @State col1: Color = Color.Yellow;
886  @State col2: Color = Color.Green;
887  @State count: number = 1;
888  build() {
889    Column() {
890      // 应避免直接在Text组件内改变count的值
891      Text(`${this.count++}`)
892        .width(50)
893        .height(50)
894        .fontColor(this.col1)
895        .onClick(() => {
896          this.col2 = Color.Red;
897        })
898      Button("change col1").onClick(() =>{
899        this.col1 = Color.Pink;
900      })
901    }
902    .backgroundColor(this.col2)
903  }
904}
905```
906
907在ArkUI中,Text('${this.count++}')在全量更新或最小化更新会产生不同的影响:
908
909- 全量更新: ArkUI可能会陷入一个无限的重渲染的循环里,因为Text组件的每一次渲染都会改变应用的状态,就会再引起下一轮渲染的开启。 当 this.col2 更改时,都会执行整个build构建函数,因此,Text(`${this.count++}`)绑定的文本也会更改,每次重新渲染Text(`${this.count++}`),又会使this.count状态变量更新,导致新一轮的build执行,从而陷入无限循环。
910
911- 最小化更新: 当 this.col2 更改时,只有Column组件会更新,Text组件不会更改。 只当 this.col1 更改时,会去更新整个Text组件,其所有属性函数都会执行,所以会看到Text(`${this.count++}`)自增。因为目前UI以组件为单位进行更新,如果组件上某一个属性发生改变,会更新整体的组件。所以整体的更新链路是:this.col2 = Color.Red -> Text组件整体更新->this.count++->Text组件整体更新。
912
913
914### 推荐用法
915
916建议应用的开发方法在事件处理程序中执行count++操作。
917
918
919```ts
920@Entry
921@Component
922struct CompA {
923  @State col1: Color = Color.Yellow;
924  @State col2: Color = Color.Green;
925  @State count: number = 1;
926  build() {
927    Column() {
928      Text(`${this.count}`)
929        .width(50)
930        .height(50)
931        .backgroundColor(this.col1)
932        .onClick(() => {
933          this.count++;
934        })
935    }
936    .backgroundColor(this.col2)
937  }
938}
939```
940
941build函数中更改应用状态的行为可能会比上面的示例更加隐蔽,比如:
942
943- 在\@Builder,\@Extend或\@Styles方法内改变状态变量 。
944
945- 在计算参数时调用函数中改变应用状态变量,例如 Text('${this.calcLabel()}')。
946
947- 对当前数组做出修改,sort()改变了数组this.arr,随后的filter方法会返回一个新的数组。
948
949
950```ts
951@State arr : Array<...> = [ ... ];
952ForEach(this.arr.sort().filter(...),
953  item => {
954  ...
955})
956```
957
958正确的执行方式为:filter返回一个新数组,后面的sort方法才不会改变原数组this.arr,示例:
959
960
961```ts
962ForEach(this.arr.filter(...).sort(),
963  item => {
964  ...
965})
966```
967
968
969## 使用状态变量强行更新
970
971
972### 不推荐用法
973
974
975```ts
976@Entry
977@Component
978struct CompA {
979  @State needsUpdate: boolean = true;
980  realState1: Array<number> = [4, 1, 3, 2]; // 未使用状态变量装饰器
981  realState2: Color = Color.Yellow;
982
983  updateUI1(param: Array<number>): Array<number> {
984    const triggerAGet = this.needsUpdate;
985    return param;
986  }
987  updateUI2(param: Color): Color {
988    const triggerAGet = this.needsUpdate;
989    return param;
990  }
991  build() {
992    Column({ space: 20 }) {
993      ForEach(this.updateUI1(this.realState1),
994        (item: Array<number>) => {
995          Text(`${item}`)
996        })
997      Text("add item")
998        .onClick(() => {
999          // 改变realState1不会触发UI视图更新
1000          this.realState1.push(this.realState1[this.realState1.length-1] + 1);
1001
1002          // 触发UI视图更新
1003          this.needsUpdate = !this.needsUpdate;
1004        })
1005      Text("chg color")
1006        .onClick(() => {
1007          // 改变realState2不会触发UI视图更新
1008          this.realState2 = this.realState2 == Color.Yellow ? Color.Red : Color.Yellow;
1009
1010          // 触发UI视图更新
1011          this.needsUpdate = !this.needsUpdate;
1012        })
1013    }.backgroundColor(this.updateUI2(this.realState2))
1014    .width(200).height(500)
1015  }
1016}
1017```
1018
1019上述示例存在以下问题:
1020
1021- 应用程序希望控制UI更新逻辑,但在ArkUI中,UI更新的逻辑应该是由框架来检测应用程序状态变量的更改去实现。
1022
1023- this.needsUpdate是一个自定义的UI状态变量,应该仅应用于其绑定的UI组件。变量this.realState1this.realState2没有被装饰,他们的变化将不会触发UI刷新。
1024
1025- 但是在该应用中,用户试图通过this.needsUpdate的更新来带动常规变量this.realState1this.realState2的更新。此方法不合理且更新性能较差,如果只想更新背景颜色,且不需要更新ForEach,但this.needsUpdate值的变化也会带动ForEach更新。
1026
1027
1028### 推荐用法
1029
1030要解决此问题,应将realState1和realState2成员变量用\@State装饰。一旦完成此操作,就不再需要变量needsUpdate。
1031
1032
1033```ts
1034@Entry
1035@Component
1036struct CompA {
1037  @State realState1: Array<number> = [4, 1, 3, 2];
1038  @State realState2: Color = Color.Yellow;
1039  build() {
1040    Column({ space: 20 }) {
1041      ForEach(this.realState1,
1042        (item: Array<number>) => {
1043          Text(`${item}`)
1044        })
1045      Text("add item")
1046        .onClick(() => {
1047          // 改变realState1触发UI视图更新
1048          this.realState1.push(this.realState1[this.realState1.length-1] + 1);
1049        })
1050      Text("chg color")
1051        .onClick(() => {
1052          // 改变realState2触发UI视图更新
1053          this.realState2 = this.realState2 == Color.Yellow ? Color.Red : Color.Yellow;
1054        })
1055    }.backgroundColor(this.realState2)
1056    .width(200).height(500)
1057  }
1058}
1059```
1060
1061## 精准控制状态变量关联的组件数
1062
1063精准控制状态变量关联的组件数能减少不必要的组件刷新,提高组件的刷新效率。有时开发者会将同一个状态变量绑定多个同级组件的属性,当状态变量改变时,会让这些组件做出相同的改变,这有时会造成组件的不必要刷新,如果存在某些比较复杂的组件,则会大大影响整体的性能。但是如果将这个状态变量绑定在这些同级组件的父组件上,则可以减少需要刷新的组件数,从而提高刷新的性能。
1064
1065### 不推荐用法
1066
1067```ts
1068@Observed
1069class Translate {
1070  translateX: number = 20;
1071}
1072@Component
1073struct Title {
1074  @ObjectLink translateObj: Translate;
1075  build() {
1076    Row() {
1077      Image($r('app.media.icon'))
1078        .width(50)
1079        .height(50)
1080        .translate({
1081          x:this.translateObj.translateX // this.translateObj.translateX used in two component both in Row
1082        })
1083      Text("Title")
1084        .fontSize(20)
1085        .translate({
1086          x: this.translateObj.translateX
1087        })
1088    }
1089  }
1090}
1091@Entry
1092@Component
1093struct Page {
1094  @State translateObj: Translate = new Translate();
1095  build() {
1096    Column() {
1097      Title({
1098        translateObj: this.translateObj
1099      })
1100      Stack() {
1101      }
1102      .backgroundColor("black")
1103      .width(200)
1104      .height(400)
1105      .translate({
1106        x:this.translateObj.translateX //this.translateObj.translateX used in two components both in Column
1107      })
1108      Button("move")
1109        .translate({
1110          x:this.translateObj.translateX
1111        })
1112        .onClick(() => {
1113          animateTo({
1114            duration: 50
1115          },()=>{
1116            this.translateObj.translateX = (this.translateObj.translateX + 50) % 150
1117          })
1118        })
1119    }
1120  }
1121}
1122```
1123
1124在上面的示例中,状态变量this.translateObj.translateX被用在多个同级的子组件下,当this.translateObj.translateX变化时,会导致所有关联它的组件一起刷新,但实际上由于这些组件的变化是相同的,因此可以将这个属性绑定到他们共同的父组件上,来实现减少组件的刷新数量。经过分析,所有的子组件其实都处于Page下的Column中,因此将所有子组件相同的translate属性统一到Column上,来实现精准控制状态变量关联的组件数。
1125
1126### 推荐用法
1127
1128```
1129@Observed
1130class Translate {
1131  translateX: number = 20;
1132}
1133@Component
1134struct Title {
1135  @ObjectLink translateObj: Translate;
1136  build() {
1137    Row() {
1138      Image($r('app.media.icon'))
1139        .width(50)
1140        .height(50)
1141      Text("Title")
1142        .fontSize(20)
1143    }
1144  }
1145}
1146@Entry
1147@Component
1148struct Page {
1149  @State translateObj: Translate = new Translate();
1150  build() {
1151    Column() {
1152      Title({
1153        translateObj: this.translateObj
1154      })
1155      Stack() {
1156      }
1157      .backgroundColor("black")
1158      .width(200)
1159      .height(400)
1160      Button("move")
1161        .onClick(() => {
1162          animateTo({
1163            duration: 50
1164          },()=>{
1165            this.translateObj.translateX = (this.translateObj.translateX + 50) % 150
1166          })
1167        })
1168    }
1169    .translate({ // the component in Column shares the same property translate
1170      x: this.translateObj.translateX
1171    })
1172  }
1173}
1174```