1# \@Observed装饰器和\@ObjectLink装饰器:嵌套类对象属性变化
2
3
4上文所述的装饰器仅能观察到第一层的变化,但是在实际应用开发中,应用会根据开发需要,封装自己的数据模型。对于多层嵌套的情况,比如二维数组,或者数组项class,或者class的属性是class,他们的第二层的属性变化是无法观察到的。这就引出了\@Observed/\@ObjectLink装饰器。
5
6\@Observed/\@ObjectLink配套使用是用于嵌套场景的观察,主要是为了弥补装饰器仅能观察一层的能力限制,开发者最好对装饰器的基本观察能力有一定的了解,再来对比阅读该文档。建议提前阅读:[\@State](./arkts-state.md)的基本用法。
7
8> **说明:**
9>
10> 从API version 9开始,这两个装饰器支持在ArkTS卡片中使用。
11>
12> 从API version 11开始,这两个装饰器支持在原子化服务中使用。
13
14## 概述
15
16\@ObjectLink和\@Observed类装饰器用于在涉及嵌套对象或数组的场景中进行双向数据同步:
17
18- 使用new创建被\@Observed装饰的类,可以被观察到属性的变化;
19
20- 子组件中\@ObjectLink装饰器装饰的状态变量用于接收\@Observed装饰的类的实例,和父组件中对应的状态变量建立双向数据绑定。这个实例可以是数组中的被\@Observed装饰的项,或者是class object中的属性,这个属性同样也需要被\@Observed装饰。
21
22- \@Observed用于嵌套类场景中,观察对象类属性变化,要配合自定义组件使用(示例详见[嵌套对象](#嵌套对象)),如果要做数据双/单向同步,需要搭配\@ObjectLink或者\@Prop使用(示例详见[\@Prop与\@ObjectLink的差异](#prop与objectlink的差异))。
23
24
25## 装饰器说明
26
27| \@Observed类装饰器 | 说明                                |
28| -------------- | --------------------------------- |
29| 装饰器参数          | 无                                 |
30| 类装饰器           | 装饰class。需要放在class的定义前,使用new创建类对象。 |
31
32| \@ObjectLink变量装饰器 | 说明                                       |
33| ----------------- | ---------------------------------------- |
34| 装饰器参数             | 无                                        |
35| 允许装饰的变量类型         | 必须为被\@Observed装饰的class实例,必须指定类型。<br/>\@ObjectLink不支持简单类型,如果开发者需要使用简单类型,可以使用[\@Prop](arkts-prop.md)。<br/>支持继承Date、[Array](#二维数组)的class实例,API11及以上支持继承[Map](#继承map类)、[Set](#继承set类)的class实例。示例见[观察变化](#观察变化)。<br/>API11及以上支持\@Observed装饰类和undefined或null组成的联合类型,比如ClassA \| ClassB, ClassA \| undefined 或者 ClassA \| null, 示例见[@ObjectLink支持联合类型](#objectlink支持联合类型)。<br/>\@ObjectLink的属性是可以改变的,但是变量的分配是不允许的,也就是说这个装饰器装饰变量是只读的,不能被改变。 |
36| 被装饰变量的初始值         | 不允许。                                     |
37
38\@ObjectLink装饰的数据为可读示例。
39
40
41```ts
42// 允许@ObjectLink装饰的数据属性赋值
43this.objLink.a= ...
44// 不允许@ObjectLink装饰的数据自身赋值
45this.objLink= ...
46```
47
48> **说明:**
49>
50> \@ObjectLink装饰的变量不能被赋值,如果要使用赋值操作,请使用[@Prop](arkts-prop.md)。
51>
52> - \@Prop装饰的变量和数据源的关系是是单向同步,\@Prop装饰的变量在本地拷贝了数据源,所以它允许本地更改,如果父组件中的数据源有更新,\@Prop装饰的变量本地的修改将被覆盖;
53>
54> - \@ObjectLink装饰的变量和数据源的关系是双向同步,\@ObjectLink装饰的变量相当于指向数据源的指针。禁止对\@ObjectLink装饰的变量赋值,如果一旦发生\@ObjectLink装饰的变量的赋值,则同步链将被打断。因为\@ObjectLink装饰的变量通过数据源(Object)引用来初始化。对于实现双向数据同步的@ObjectLink,赋值相当于更新父组件中的数组项或者class的属性,TypeScript/JavaScript不能实现,会发生运行时报错。
55
56
57## 变量的传递/访问规则说明
58
59| \@ObjectLink传递/访问 | 说明                                       |
60| ----------------- | ---------------------------------------- |
61| 从父组件初始化           | 必须指定。<br/>初始化\@ObjectLink装饰的变量必须同时满足以下场景:<br/>-&nbsp;类型必须是\@Observed装饰的class。<br/>-&nbsp;初始化的数值需要是数组项,或者class的属性。<br/>-&nbsp;同步源的class或者数组必须是[\@State](./arkts-state.md),[\@Link](./arkts-link.md),[\@Provide](./arkts-provide-and-consume.md),[\@Consume](./arkts-provide-and-consume.md)或者\@ObjectLink装饰的数据。<br/>同步源是数组项的示例请参考[对象数组](#对象数组)。初始化的class的示例请参考[嵌套对象](#嵌套对象)。 |
62| 与源对象同步            | 双向。                                      |
63| 可以初始化子组件          | 允许,可用于初始化常规变量、\@State、\@Link、\@Prop、\@Provide |
64
65
66  **图1** 初始化规则图示  
67
68
69![zh-cn_image_0000001502255262](figures/zh-cn_image_0000001502255262.png)
70
71
72## 观察变化和行为表现
73
74
75### 观察变化
76
77\@Observed装饰的类,如果其属性为非简单类型,比如class、Object或者数组,也需要被\@Observed装饰,否则将观察不到其属性的变化。
78
79
80```ts
81class Child {
82  public num: number;
83
84  constructor(num: number) {
85    this.num = num;
86  }
87}
88
89@Observed
90class Parent {
91  public child: Child;
92  public count: number;
93
94  constructor(child: Child, count: number) {
95    this.child = child;
96    this.count = count;
97  }
98}
99```
100
101以上示例中,Parent被\@Observed装饰,其成员变量的赋值的变化是可以被观察到的,但对于Child,没有被\@Observed装饰,其属性的修改不能被观察到。
102
103
104```ts
105@ObjectLink parent: Parent;
106
107// 赋值变化可以被观察到
108this.parent.child = new Child(5);
109this.parent.count = 5;
110
111// Child没有被@Observed装饰,其属性的变化观察不到
112this.parent.child.num = 5;
113```
114
115\@ObjectLink:\@ObjectLink只能接收被\@Observed装饰class的实例,推荐设计单独的自定义组件来渲染每一个数组或对象。此时,对象数组或嵌套对象(属性是对象的对象称为嵌套对象)需要两个自定义组件,一个自定义组件呈现外部数组/对象,另一个自定义组件呈现嵌套在数组/对象内的类对象。可以观察到:
116
117- 其属性的数值的变化,其中属性是指Object.keys(observedObject)返回的所有属性,示例请参考[嵌套对象](#嵌套对象)。
118
119- 如果数据源是数组,则可以观察到数组item的替换,如果数据源是class,可观察到class的属性的变化,示例请参考[对象数组](#对象数组)。
120
121继承Date的class时,可以观察到Date整体的赋值,同时可通过调用Date的接口`setFullYear`, `setMonth`, `setDate`, `setHours`, `setMinutes`, `setSeconds`, `setMilliseconds`, `setTime`, `setUTCFullYear`, `setUTCMonth`, `setUTCDate`, `setUTCHours`, `setUTCMinutes`, `setUTCSeconds`, `setUTCMilliseconds` 更新Date的属性。
122
123```ts
124@Observed
125class DateClass extends Date {
126  constructor(args: number | string) {
127    super(args);
128  }
129}
130
131@Observed
132class NewDate {
133  public data: DateClass;
134
135  constructor(data: DateClass) {
136    this.data = data;
137  }
138}
139
140@Component
141struct Child {
142  label: string = 'date';
143  @ObjectLink data: DateClass;
144
145  build() {
146    Column() {
147      Button(`child increase the day by 1`)
148        .onClick(() => {
149          this.data.setDate(this.data.getDate() + 1);
150        })
151      DatePicker({
152        start: new Date('1970-1-1'),
153        end: new Date('2100-1-1'),
154        selected: this.data
155      })
156    }
157  }
158}
159
160@Entry
161@Component
162struct Parent {
163  @State newData: NewDate = new NewDate(new DateClass('2023-1-1'));
164
165  build() {
166    Column() {
167      Child({ label: 'date', data: this.newData.data })
168
169      Button(`parent update the new date`)
170        .onClick(() => {
171          this.newData.data = new DateClass('2023-07-07');
172        })
173      Button(`ViewB: this.newData = new NewDate(new DateClass('2023-08-20'))`)
174        .onClick(() => {
175          this.newData = new NewDate(new DateClass('2023-08-20'));
176        })
177    }
178  }
179}
180```
181
182继承Map的class时,可以观察到Map整体的赋值,同时可通过调用Map的接口`set`, `clear`, `delete` 更新Map的值。详见[继承Map类](#继承map类)。
183
184继承Set的class时,可以观察到Set整体的赋值,同时可通过调用Set的接口`add`, `clear`, `delete` 更新Set的值。详见[继承Set类](#继承set类)。
185
186
187### 框架行为
188
1891. 初始渲染:
190
191   a. \@Observed装饰的class的实例会被不透明的代理对象包装,代理了class上的属性的setter和getter方法。
192
193   b. 子组件中\@ObjectLink装饰的从父组件初始化,接收被\@Observed装饰的class的实例,\@ObjectLink的包装类会将自己注册给\@Observed class。
194
1952. 属性更新:当\@Observed装饰的class属性改变时,会执行到代理的setter和getter,然后遍历依赖它的\@ObjectLink包装类,通知数据更新。
196
197
198## 限制条件
199
2001. 使用\@Observed装饰class会改变class原始的原型链,\@Observed和其他类装饰器装饰同一个class可能会带来问题。
201
2022. \@ObjectLink装饰器不能在\@Entry装饰的自定义组件中使用。
203
2043. \@ObjectLink装饰的变量类型需要为显式的被@Observed装饰的类,如果未指定类型,或其不是\@Observed装饰的class,编译期会报错。
205
206  ```ts
207  @Observed
208  class Info {
209    count: number;
210
211    constructor(count: number) {
212      this.count = count;
213    }
214  }
215
216  class Test {
217    msg: number;
218
219    constructor(msg: number) {
220      this.msg = msg;
221    }
222  }
223
224  // 错误写法,count未指定类型,编译报错
225  @ObjectLink count;
226  // 错误写法,Test未被@Observed装饰,编译报错
227  @ObjectLink test: Test;
228
229  // 正确写法
230  @ObjectLink count: Info;
231  ```
232
2334. \@ObjectLink装饰的变量不能本地初始化,仅能通过构造参数从父组件传入初始值,否则编译期会报错。
234
235  ```ts
236  @Observed
237  class Info {
238    count: number;
239
240    constructor(count: number) {
241      this.count = count;
242    }
243  }
244
245  // 错误写法,编译报错
246  @ObjectLink count: Info = new Info(10);
247
248  // 正确写法
249  @ObjectLink count: Info;
250  ```
251
2525. \@ObjectLink装饰的变量是只读的,不能被赋值,否则会有运行时报错提示Cannot set property when setter is undefined。如果需要对\@ObjectLink装饰的变量进行整体替换,可以在父组件对其进行整体替换。
253
254  【反例】
255
256  ```ts
257  @Observed
258  class Info {
259    count: number;
260
261    constructor(count: number) {
262      this.count = count;
263    }
264  }
265
266  @Component
267  struct Child {
268    @ObjectLink num: Info;
269
270    build() {
271      Column() {
272        Text(`num的值: ${this.num.count}`)
273          .onClick(() => {
274            // 错误写法,@ObjectLink装饰的变量不能被赋值
275            this.num = new Info(10);
276          })
277      }
278    }
279  }
280
281  @Entry
282  @Component
283  struct Parent {
284    @State num: Info = new Info(10);
285
286    build() {
287      Column() {
288        Text(`count的值: ${this.num.count}`)
289        Child({num: this.num})
290      }
291    }
292  }
293  ```
294
295  【正例】
296
297  ```ts
298  @Observed
299  class Info {
300    count: number;
301
302    constructor(count: number) {
303      this.count = count;
304    }
305  }
306
307  @Component
308  struct Child {
309    @ObjectLink num: Info;
310
311    build() {
312      Column() {
313        Text(`num的值: ${this.num.count}`)
314          .onClick(() => {
315            // 正确写法,可以更改@ObjectLink装饰变量的成员属性
316            this.num.count = 20;
317          })
318      }
319    }
320  }
321
322  @Entry
323  @Component
324  struct Parent {
325    @State num: Info = new Info(10);
326
327    build() {
328      Column() {
329        Text(`count的值: ${this.num.count}`)
330        Button('click')
331          .onClick(() => {
332            // 可以在父组件做整体替换
333            this.num = new Info(30);
334          })
335        Child({num: this.num})
336      }
337    }
338  }
339  ```
340
341
342## 使用场景
343
344
345### 嵌套对象
346
347> **说明:**
348>
349> NextID是用来在[ForEach循环渲染](./arkts-rendering-control-foreach.md)过程中,为每个数组元素生成一个唯一且持久的键值,用于标识对应的组件。
350
351
352```ts
353// objectLinkNestedObjects.ets
354let NextID: number = 1;
355
356@Observed
357class Bag {
358  public id: number;
359  public size: number;
360
361  constructor(size: number) {
362    this.id = NextID++;
363    this.size = size;
364  }
365}
366
367@Observed
368class User {
369  public bag: Bag;
370
371  constructor(bag: Bag) {
372    this.bag = bag;
373  }
374}
375
376@Observed
377class Book {
378  public bookName: BookName;
379
380  constructor(bookName: BookName) {
381    this.bookName = bookName;
382  }
383}
384
385@Observed
386class BookName extends Bag {
387  public nameSize: number;
388
389  constructor(nameSize: number) {
390    // 调用父类方法对nameSize进行处理
391    super(nameSize);
392    this.nameSize = nameSize;
393  }
394}
395
396@Component
397struct Son {
398  label: string = 'Son';
399  @ObjectLink bag: Bag;
400
401  build() {
402    Column() {
403      Text(`Son [${this.label}] this.bag.size = ${this.bag.size}`)
404        .fontColor('#ffffffff')
405        .backgroundColor('#ff3d9dba')
406        .width(320)
407        .height(50)
408        .borderRadius(25)
409        .margin(10)
410        .textAlign(TextAlign.Center)
411      Button(`Son: this.bag.size add 1`)
412        .width(320)
413        .backgroundColor('#ff17a98d')
414        .margin(10)
415        .onClick(() => {
416          this.bag.size += 1;
417        })
418    }
419  }
420}
421
422@Component
423struct Father {
424  label: string = 'Father';
425  @ObjectLink bookName: BookName;
426
427  build() {
428    Row() {
429      Column() {
430        Text(`Father [${this.label}] this.bookName.size = ${this.bookName.size}`)
431          .fontColor('#ffffffff')
432          .backgroundColor('#ff3d9dba')
433          .width(320)
434          .height(50)
435          .borderRadius(25)
436          .margin(10)
437          .textAlign(TextAlign.Center)
438        Button(`Father: this.bookName.size add 1`)
439          .width(320)
440          .backgroundColor('#ff17a98d')
441          .margin(10)
442          .onClick(() => {
443            this.bookName.size += 1;
444            console.log('this.bookName.size:' + this.bookName.size);
445          })
446      }
447      .width(320)
448    }
449  }
450}
451
452@Entry
453@Component
454struct GrandFather {
455  @State user: User = new User(new Bag(0));
456  @State child: Book = new Book(new BookName(0));
457
458  build() {
459    Column() {
460      Son({ label: 'Son #1', bag: this.user.bag })
461        .width(320)
462      Father({ label: 'Father #3', bookName: this.child.bookName })
463        .width(320)
464      Button(`GrandFather: this.child.bookName.size add 10`)
465        .width(320)
466        .backgroundColor('#ff17a98d')
467        .margin(10)
468        .onClick(() => {
469          this.child.bookName.size += 10;
470          console.log('this.child.bookName.size:' + this.child.bookName.size);
471        })
472      Button(`GrandFather: this.user.bag = new Bag(10)`)
473        .width(320)
474        .backgroundColor('#ff17a98d')
475        .margin(10)
476        .onClick(() => {
477          this.user.bag = new Bag(10);
478        })
479      Button(`GrandFather: this.user = new User(new Bag(20))`)
480        .width(320)
481        .backgroundColor('#ff17a98d')
482        .margin(10)
483        .onClick(() => {
484          this.user = new User(new Bag(20));
485        })
486    }
487  }
488}
489```
490
491![Observed_ObjectLink_nested_object](figures/Observed_ObjectLink_nested_object.gif)
492
493被@Observed装饰的BookName类,可以观测到继承基类的属性的变化。
494
495
496GrandFather中的事件句柄:
497
498
499- this.user.bag = new Bag(10) 和this.user = new User(new Bag(20)): 对@State装饰的变量user和其属性的修改。
500
501- this.child.bookName.size += ... :该变化属于第二层的变化,@State无法观察到第二层的变化,但是Bag被\@Observed装饰,Bag的属性size的变化可以被\@ObjectLink观察到。
502
503
504Father中的事件句柄:
505
506
507- this.bookName.size += 1:对\@ObjectLink变量size的修改,将触发Text组件的刷新。\@ObjectLink和\@Prop不同,\@ObjectLink不拷贝来自父组件的数据源,而是在本地构建了指向其数据源的引用。
508
509- \@ObjectLink变量是只读的,this.bookName = new bookName(...)是不允许的,因为一旦赋值操作发生,指向数据源的引用将被重置,同步将被打断。
510
511
512### 对象数组
513
514对象数组是一种常用的数据结构。以下示例展示了数组对象的用法。
515
516
517```ts
518let NextID: number = 1;
519
520@Observed
521class Info {
522  public id: number;
523  public info: number;
524
525  constructor(info: number) {
526    this.id = NextID++;
527    this.info = info;
528  }
529}
530
531@Component
532struct Child {
533  // 子组件Child的@ObjectLink的类型是Info
534  @ObjectLink info: Info;
535  label: string = 'ViewChild';
536
537  build() {
538    Row() {
539      Button(`ViewChild [${this.label}] this.info.info = ${this.info ? this.info.info : "undefined"}`)
540        .width(320)
541        .margin(10)
542        .onClick(() => {
543          this.info.info += 1;
544        })
545    }
546  }
547}
548
549@Entry
550@Component
551struct Parent {
552  // Parent中有@State装饰的Info[]
553  @State arrA: Info[] = [new Info(0), new Info(0)];
554
555  build() {
556    Column() {
557      ForEach(this.arrA,
558        (item: Info) => {
559          Child({ label: `#${item.id}`, info: item })
560        },
561        (item: Info): string => item.id.toString()
562      )
563      // 使用@State装饰的数组的数组项初始化@ObjectLink,其中数组项是被@Observed装饰的Info的实例
564      Child({ label: `ViewChild this.arrA[first]`, info: this.arrA[0] })
565      Child({ label: `ViewChild this.arrA[last]`, info: this.arrA[this.arrA.length-1] })
566
567      Button(`ViewParent: reset array`)
568        .width(320)
569        .margin(10)
570        .onClick(() => {
571          this.arrA = [new Info(0), new Info(0)];
572        })
573      Button(`ViewParent: push`)
574        .width(320)
575        .margin(10)
576        .onClick(() => {
577          this.arrA.push(new Info(0));
578        })
579      Button(`ViewParent: shift`)
580        .width(320)
581        .margin(10)
582        .onClick(() => {
583          if (this.arrA.length > 0) {
584            this.arrA.shift();
585          } else {
586            console.log("length <= 0");
587          }
588        })
589      Button(`ViewParent: item property in middle`)
590        .width(320)
591        .margin(10)
592        .onClick(() => {
593          this.arrA[Math.floor(this.arrA.length / 2)].info = 10;
594        })
595      Button(`ViewParent: item property in middle`)
596        .width(320)
597        .margin(10)
598        .onClick(() => {
599          this.arrA[Math.floor(this.arrA.length / 2)] = new Info(11);
600        })
601    }
602  }
603}
604```
605
606![Observed_ObjectLink_object_array](figures/Observed_ObjectLink_object_array.gif)
607
608- this.arrA[Math.floor(this.arrA.length/2)] = new Info(..) :该状态变量的改变触发2次更新:
609  1. ForEach:数组项的赋值导致ForEach的[itemGenerator](../reference/apis-arkui/arkui-ts/ts-rendering-control-foreach.md)被修改,因此数组项被识别为有更改,ForEach的item builder将执行,创建新的Child组件实例。
610  2. Child({ label: `ViewChild this.arrA[last]`, info: this.arrA[this.arrA.length-1] }):上述更改改变了数组中第二个元素,所以绑定this.arrA[1]的Child将被更新。
611
612- this.arrA.push(new Info(0)) : 将触发2次不同效果的更新:
613  1. ForEach:新添加的Info对象对于ForEach是未知的[itemGenerator](../reference/apis-arkui/arkui-ts/ts-rendering-control-foreach.md),ForEach的item builder将执行,创建新的Child组件实例。
614  2. Child({ label: `ViewChild this.arrA[last]`, info: this.arrA[this.arrA.length-1] }):数组的最后一项有更改,因此引起第二个Child的实例的更改。对于Child({ label: `ViewChild this.arrA[first]`, info: this.arrA[0] }),数组的更改并没有触发一个数组项更改的改变,所以第一个Child不会刷新。
615
616- this.arrA[Math.floor(this.arrA.length/2)].info:@State无法观察到第二层的变化,但是Info被\@Observed装饰,Info的属性的变化将被\@ObjectLink观察到。
617
618
619### 二维数组
620
621使用\@Observed观察二维数组的变化。可以声明一个被\@Observed装饰的继承Array的子类。
622
623
624```ts
625@Observed
626class ObservedArray<T> extends Array<T> {
627  constructor(args: T[]) {
628    super(...args);
629  }
630}
631```
632
633声明一个继承自Array的类ObservedArray\<T\>并使用new操作符创建ObservedArray\<string\>的实例。通过new操作符创建的ObservedArray的实例可以观察到属性变化。
634
635在下面的示例中,展示了如何利用\@Observed观察二维数组的变化。
636
637```ts
638@Observed
639class ObservedArray<T> extends Array<T> {
640  constructor(args: T[]) {
641    super(...args);
642  }
643}
644
645@Component
646struct Item {
647  @ObjectLink itemArr: ObservedArray<string>;
648
649  build() {
650    Row() {
651      ForEach(this.itemArr, (item: string, index: number) => {
652        Text(`${index}: ${item}`)
653          .width(100)
654          .height(100)
655      }, (item: string) => item)
656    }
657  }
658}
659
660@Entry
661@Component
662struct IndexPage {
663  @State arr: Array<ObservedArray<string>> = [new ObservedArray<string>(['apple']), new ObservedArray<string>(['banana']), new ObservedArray<string>(['orange'])];
664
665  build() {
666    Column() {
667      ForEach(this.arr, (itemArr: ObservedArray<string>) => {
668        Item({ itemArr: itemArr })
669      })
670
671      Divider()
672
673      Button('push two-dimensional array item')
674        .margin(10)
675        .onClick(() => {
676          this.arr[0].push('strawberry');
677        })
678
679      Button('push array item')
680        .margin(10)
681        .onClick(() => {
682          this.arr.push(new ObservedArray<string>(['pear']));
683        })
684
685      Button('change two-dimensional array first item')
686        .margin(10)
687        .onClick(() => {
688          this.arr[0][0] = 'APPLE';
689        })
690
691      Button('change array first item')
692        .margin(10)
693        .onClick(() => {
694          this.arr[0] = new ObservedArray<string>(['watermelon']);
695        })
696    }
697  }
698}
699```
700
701![Observed_ObjectLink_2D_array](figures/Observed_ObjectLink_2D_array.gif)
702
703### 继承Map类
704
705> **说明:**
706>
707> 从API version 11开始,\@ObjectLink支持\@Observed装饰Map类型和继承Map类的类型。
708
709在下面的示例中,myMap类型为MyMap\<number, string\>,点击Button改变myMap的属性,视图会随之刷新。
710
711```ts
712@Observed
713class Info {
714  public info: MyMap<number, string>;
715
716  constructor(info: MyMap<number, string>) {
717    this.info = info;
718  }
719}
720
721
722@Observed
723export class MyMap<K, V> extends Map<K, V> {
724  public name: string;
725
726  constructor(name?: string, args?: [K, V][]) {
727    super(args);
728    this.name = name ? name : "My Map";
729  }
730
731  getName() {
732    return this.name;
733  }
734}
735
736@Entry
737@Component
738struct MapSampleNested {
739  @State message: Info = new Info(new MyMap("myMap", [[0, "a"], [1, "b"], [3, "c"]]));
740
741  build() {
742    Row() {
743      Column() {
744        MapSampleNestedChild({ myMap: this.message.info })
745      }
746      .width('100%')
747    }
748    .height('100%')
749  }
750}
751
752@Component
753struct MapSampleNestedChild {
754  @ObjectLink myMap: MyMap<number, string>;
755
756  build() {
757    Row() {
758      Column() {
759        ForEach(Array.from(this.myMap.entries()), (item: [number, string]) => {
760          Text(`${item[0]}`).fontSize(30)
761          Text(`${item[1]}`).fontSize(30)
762          Divider().strokeWidth(5)
763        })
764
765        Button('set new one')
766          .width(200)
767          .margin(10)
768          .onClick(() => {
769            this.myMap.set(4, "d");
770          })
771        Button('clear')
772          .width(200)
773          .margin(10)
774          .onClick(() => {
775            this.myMap.clear();
776          })
777        Button('replace the first one')
778          .width(200)
779          .margin(10)
780          .onClick(() => {
781            this.myMap.set(0, "aa");
782          })
783        Button('delete the first one')
784          .width(200)
785          .margin(10)
786          .onClick(() => {
787            this.myMap.delete(0);
788          })
789      }
790      .width('100%')
791    }
792    .height('100%')
793  }
794}
795```
796
797![Observed_ObjectLink_inherit_map](figures/Observed_ObjectLink_inherit_map.gif)
798
799### 继承Set类
800
801> **说明:**
802>
803> 从API version 11开始,\@ObjectLink支持\@Observed装饰Set类型和继承Set类的类型。
804
805在下面的示例中,mySet类型为MySet\<number\>,点击Button改变mySet的属性,视图会随之刷新。
806
807```ts
808@Observed
809class Info {
810  public info: MySet<number>;
811
812  constructor(info: MySet<number>) {
813    this.info = info;
814  }
815}
816
817
818@Observed
819export class MySet<T> extends Set<T> {
820  public name: string;
821
822  constructor(name?: string, args?: T[]) {
823    super(args);
824    this.name = name ? name : "My Set";
825  }
826
827  getName() {
828    return this.name;
829  }
830}
831
832@Entry
833@Component
834struct SetSampleNested {
835  @State message: Info = new Info(new MySet("Set", [0, 1, 2, 3, 4]));
836
837  build() {
838    Row() {
839      Column() {
840        SetSampleNestedChild({ mySet: this.message.info })
841      }
842      .width('100%')
843    }
844    .height('100%')
845  }
846}
847
848@Component
849struct SetSampleNestedChild {
850  @ObjectLink mySet: MySet<number>;
851
852  build() {
853    Row() {
854      Column() {
855        ForEach(Array.from(this.mySet.entries()), (item: [number, number]) => {
856          Text(`${item}`).fontSize(30)
857          Divider()
858        })
859        Button('set new one')
860          .width(200)
861          .margin(10)
862          .onClick(() => {
863            this.mySet.add(5);
864          })
865        Button('clear')
866          .width(200)
867          .margin(10)
868          .onClick(() => {
869            this.mySet.clear();
870          })
871        Button('delete the first one')
872          .width(200)
873          .margin(10)
874          .onClick(() => {
875            this.mySet.delete(0);
876          })
877      }
878      .width('100%')
879    }
880    .height('100%')
881  }
882}
883```
884
885![Observed_ObjectLink_inherit_set](figures/Observed_ObjectLink_inherit_set.gif)
886
887## ObjectLink支持联合类型
888
889@ObjectLink支持@Observed装饰类和undefined或null组成的联合类型,在下面的示例中,count类型为Source | Data | undefined,点击父组件Parent中的Button改变count的属性或者类型,Child中也会对应刷新。
890
891```ts
892@Observed
893class Source {
894  public source: number;
895
896  constructor(source: number) {
897    this.source = source;
898  }
899}
900
901@Observed
902class Data {
903  public data: number;
904
905  constructor(data: number) {
906    this.data = data;
907  }
908}
909
910@Entry
911@Component
912struct Parent {
913  @State count: Source | Data | undefined = new Source(10);
914
915  build() {
916    Column() {
917      Child({ count: this.count })
918
919      Button('change count property')
920        .margin(10)
921        .onClick(() => {
922          // 判断count的类型,做属性的更新
923          if (this.count instanceof Source) {
924            this.count.source += 1;
925          } else if (this.count instanceof Data) {
926            this.count.data += 1;
927          } else {
928            console.info('count is undefined, cannot change property');
929          }
930        })
931
932      Button('change count to Source')
933        .margin(10)
934        .onClick(() => {
935          // 赋值为Source的实例
936          this.count = new Source(100);
937        })
938
939      Button('change count to Data')
940        .margin(10)
941        .onClick(() => {
942          // 赋值为Data的实例
943          this.count = new Data(100);
944        })
945
946      Button('change count to undefined')
947        .margin(10)
948        .onClick(() => {
949          // 赋值为undefined
950          this.count = undefined;
951        })
952    }.width('100%')
953  }
954}
955
956@Component
957struct Child {
958  @ObjectLink count: Source | Data | undefined;
959
960  build() {
961    Column() {
962      Text(`count is instanceof ${this.count instanceof Source ? 'Source' :
963        this.count instanceof Data ? 'Data' : 'undefined'}`)
964        .fontSize(30)
965        .margin(10)
966
967      Text(`count's property is  ${this.count instanceof Source ? this.count.source : this.count?.data}`).fontSize(15)
968
969    }.width('100%')
970  }
971}
972```
973
974![ObjectLink-support-union-types](figures/ObjectLink-support-union-types.gif)
975
976## 常见问题
977
978### 在子组件中给@ObjectLink装饰的变量赋值
979
980在子组件中给@ObjectLink装饰的变量赋值是不允许的。
981
982【反例】
983
984```ts
985@Observed
986class Info {
987  public info: number = 0;
988
989  constructor(info: number) {
990    this.info = info;
991  }
992}
993
994@Component
995struct ObjectLinkChild {
996  @ObjectLink testNum: Info;
997
998  build() {
999    Text(`ObjectLinkChild testNum ${this.testNum.info}`)
1000      .onClick(() => {
1001        // ObjectLink不能被赋值
1002        this.testNum = new Info(47);
1003      })
1004  }
1005}
1006
1007@Entry
1008@Component
1009struct Parent {
1010  @State testNum: Info[] = [new Info(1)];
1011
1012  build() {
1013    Column() {
1014      Text(`Parent testNum ${this.testNum[0].info}`)
1015        .onClick(() => {
1016          this.testNum[0].info += 1;
1017        })
1018
1019      ObjectLinkChild({ testNum: this.testNum[0] })
1020    }
1021  }
1022}
1023```
1024
1025点击ObjectLinkChild给\@ObjectLink装饰的变量赋值:
1026
1027```
1028this.testNum = new Info(47);
1029```
1030
1031这是不允许的,对于实现双向数据同步的\@ObjectLink,赋值相当于要更新父组件中的数组项或者class的属性,这个对于 TypeScript/JavaScript是不能实现的。框架对于这种行为会发生运行时报错。
1032
1033【正例】
1034
1035```ts
1036@Observed
1037class Info {
1038  public info: number = 0;
1039
1040  constructor(info: number) {
1041    this.info = info;
1042  }
1043}
1044
1045@Component
1046struct ObjectLinkChild {
1047  @ObjectLink testNum: Info;
1048
1049  build() {
1050    Text(`ObjectLinkChild testNum ${this.testNum.info}`)
1051      .onClick(() => {
1052        // 可以对ObjectLink装饰对象的属性赋值
1053        this.testNum.info = 47;
1054      })
1055  }
1056}
1057
1058@Entry
1059@Component
1060struct Parent {
1061  @State testNum: Info[] = [new Info(1)];
1062
1063  build() {
1064    Column() {
1065      Text(`Parent testNum ${this.testNum[0].info}`)
1066        .onClick(() => {
1067          this.testNum[0].info += 1;
1068        })
1069
1070      ObjectLinkChild({ testNum: this.testNum[0] })
1071    }
1072  }
1073}
1074```
1075
1076### 基础嵌套对象属性更改失效
1077
1078在应用开发中,有很多嵌套对象场景,例如,开发者更新了某个属性,但UI没有进行对应的更新。
1079
1080每个装饰器都有自己可以观察的能力,并不是所有的改变都可以被观察到,只有可以被观察到的变化才会进行UI更新。\@Observed装饰器可以观察到嵌套对象的属性变化,其他装饰器仅能观察到第一层的变化。
1081
1082【反例】
1083
1084下面的例子中,一些UI组件并不会更新。
1085
1086
1087```ts
1088class Parent {
1089  parentId: number;
1090
1091  constructor(parentId: number) {
1092    this.parentId = parentId;
1093  }
1094
1095  getParentId(): number {
1096    return this.parentId;
1097  }
1098
1099  setParentId(parentId: number): void {
1100    this.parentId = parentId;
1101  }
1102}
1103
1104class Child {
1105  childId: number;
1106
1107  constructor(childId: number) {
1108    this.childId = childId;
1109  }
1110
1111  getChildId(): number {
1112    return this.childId;
1113  }
1114
1115  setChildId(childId: number): void {
1116    this.childId = childId;
1117  }
1118}
1119
1120class Cousin extends Parent {
1121  cousinId: number = 47;
1122  child: Child;
1123
1124  constructor(parentId: number, cousinId: number, childId: number) {
1125    super(parentId);
1126    this.cousinId = cousinId;
1127    this.child = new Child(childId);
1128  }
1129
1130  getCousinId(): number {
1131    return this.cousinId;
1132  }
1133
1134  setCousinId(cousinId: number): void {
1135    this.cousinId = cousinId;
1136  }
1137
1138  getChild(): number {
1139    return this.child.getChildId();
1140  }
1141
1142  setChild(childId: number): void {
1143    return this.child.setChildId(childId);
1144  }
1145}
1146
1147@Entry
1148@Component
1149struct MyView {
1150  @State cousin: Cousin = new Cousin(10, 20, 30);
1151
1152  build() {
1153    Column({ space: 10 }) {
1154      Text(`parentId: ${this.cousin.parentId}`)
1155      Button("Change Parent.parent")
1156        .onClick(() => {
1157          this.cousin.parentId += 1;
1158        })
1159
1160      Text(`cousinId: ${this.cousin.cousinId}`)
1161      Button("Change Cousin.cousinId")
1162        .onClick(() => {
1163          this.cousin.cousinId += 1;
1164        })
1165
1166      Text(`childId: ${this.cousin.child.childId}`)
1167      Button("Change Cousin.Child.childId")
1168        .onClick(() => {
1169          // 点击时上面的Text组件不会刷新
1170          this.cousin.child.childId += 1;
1171        })
1172    }
1173  }
1174}
1175```
1176
1177- 最后一个Text组件Text('child: ${this.cousin.child.childId}'),当点击该组件时UI不会刷新。 因为,\@State cousin : Cousin 只能观察到this.cousin属性的变化,比如this.cousin.parentId, this.cousin.cousinIdthis.cousin.child的变化,但是无法观察嵌套在属性中的属性,即this.cousin.child.childId(属性childId是内嵌在cousin中的对象Child的属性)。
1178
1179- 为了观察到嵌套于内部的Child的属性,需要做如下改变:
1180  - 构造一个子组件,用于单独渲染Child的实例。 该子组件可以使用\@ObjectLink child : Child或\@Prop child : Child。通常会使用\@ObjectLink,除非子组件需要对其Child对象进行本地修改。
1181  - 嵌套的Child必须用\@Observed装饰。当在Cousin中创建Child对象时(本示例中的Cousin(10, 20, 30)),它将被包装在ES6代理中,当Child属性更改时(this.cousin.child.childId += 1),该代码将修改通知到\@ObjectLink变量。
1182
1183【正例】
1184
1185以下示例使用\@Observed/\@ObjectLink来观察嵌套对象的属性更改。
1186
1187
1188```ts
1189class Parent {
1190  parentId: number;
1191
1192  constructor(parentId: number) {
1193    this.parentId = parentId;
1194  }
1195
1196  getParentId(): number {
1197    return this.parentId;
1198  }
1199
1200  setParentId(parentId: number): void {
1201    this.parentId = parentId;
1202  }
1203}
1204
1205@Observed
1206class Child {
1207  childId: number;
1208
1209  constructor(childId: number) {
1210    this.childId = childId;
1211  }
1212
1213  getChildId(): number {
1214    return this.childId;
1215  }
1216
1217  setChildId(childId: number): void {
1218    this.childId = childId;
1219  }
1220}
1221
1222class Cousin extends Parent {
1223  cousinId: number = 47;
1224  child: Child;
1225
1226  constructor(parentId: number, cousinId: number, childId: number) {
1227    super(parentId);
1228    this.cousinId = cousinId;
1229    this.child = new Child(childId);
1230  }
1231
1232  getCousinId(): number {
1233    return this.cousinId;
1234  }
1235
1236  setCousinId(cousinId: number): void {
1237    this.cousinId = cousinId;
1238  }
1239
1240  getChild(): number {
1241    return this.child.getChildId();
1242  }
1243
1244  setChild(childId: number): void {
1245    return this.child.setChildId(childId);
1246  }
1247}
1248
1249@Component
1250struct ViewChild {
1251  @ObjectLink child: Child;
1252
1253  build() {
1254    Column({ space: 10 }) {
1255      Text(`childId: ${this.child.getChildId()}`)
1256      Button("Change childId")
1257        .onClick(() => {
1258          this.child.setChildId(this.child.getChildId() + 1);
1259        })
1260    }
1261  }
1262}
1263
1264@Entry
1265@Component
1266struct MyView {
1267  @State cousin: Cousin = new Cousin(10, 20, 30);
1268
1269  build() {
1270    Column({ space: 10 }) {
1271      Text(`parentId: ${this.cousin.parentId}`)
1272      Button("Change Parent.parentId")
1273        .onClick(() => {
1274          this.cousin.parentId += 1;
1275        })
1276
1277      Text(`cousinId: ${this.cousin.cousinId}`)
1278      Button("Change Cousin.cousinId")
1279        .onClick(() => {
1280          this.cousin.cousinId += 1;
1281        })
1282
1283      ViewChild({ child: this.cousin.child }) // Text(`childId: ${this.cousin.child.childId}`)的替代写法
1284      Button("Change Cousin.Child.childId")
1285        .onClick(() => {
1286          this.cousin.child.childId += 1;
1287        })
1288    }
1289  }
1290}
1291```
1292
1293### 复杂嵌套对象属性更改失效
1294
1295【反例】
1296
1297以下示例创建了一个带有\@ObjectLink装饰变量的子组件,用于渲染一个含有嵌套属性的ParentCounter,用\@Observed装饰嵌套在ParentCounter中的SubCounter。
1298
1299
1300```ts
1301let nextId = 1;
1302@Observed
1303class SubCounter {
1304  counter: number;
1305  constructor(c: number) {
1306    this.counter = c;
1307  }
1308}
1309@Observed
1310class ParentCounter {
1311  id: number;
1312  counter: number;
1313  subCounter: SubCounter;
1314  incrCounter() {
1315    this.counter++;
1316  }
1317  incrSubCounter(c: number) {
1318    this.subCounter.counter += c;
1319  }
1320  setSubCounter(c: number): void {
1321    this.subCounter.counter = c;
1322  }
1323  constructor(c: number) {
1324    this.id = nextId++;
1325    this.counter = c;
1326    this.subCounter = new SubCounter(c);
1327  }
1328}
1329@Component
1330struct CounterComp {
1331  @ObjectLink value: ParentCounter;
1332  build() {
1333    Column({ space: 10 }) {
1334      Text(`${this.value.counter}`)
1335        .fontSize(25)
1336        .onClick(() => {
1337          this.value.incrCounter();
1338        })
1339      Text(`${this.value.subCounter.counter}`)
1340        .onClick(() => {
1341          this.value.incrSubCounter(1);
1342        })
1343      Divider().height(2)
1344    }
1345  }
1346}
1347@Entry
1348@Component
1349struct ParentComp {
1350  @State counter: ParentCounter[] = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)];
1351  build() {
1352    Row() {
1353      Column() {
1354        CounterComp({ value: this.counter[0] })
1355        CounterComp({ value: this.counter[1] })
1356        CounterComp({ value: this.counter[2] })
1357        Divider().height(5)
1358        ForEach(this.counter,
1359          (item: ParentCounter) => {
1360            CounterComp({ value: item })
1361          },
1362          (item: ParentCounter) => item.id.toString()
1363        )
1364        Divider().height(5)
1365        // 第一个点击事件
1366        Text('Parent: incr counter[0].counter')
1367          .fontSize(20).height(50)
1368          .onClick(() => {
1369            this.counter[0].incrCounter();
1370            // 每次触发时自增10
1371            this.counter[0].incrSubCounter(10);
1372          })
1373        // 第二个点击事件
1374        Text('Parent: set.counter to 10')
1375          .fontSize(20).height(50)
1376          .onClick(() => {
1377            // 无法将value设置为10,UI不会刷新
1378            this.counter[0].setSubCounter(10);
1379          })
1380        Text('Parent: reset entire counter')
1381          .fontSize(20).height(50)
1382          .onClick(() => {
1383            this.counter = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)];
1384          })
1385      }
1386    }
1387  }
1388}
1389```
1390
1391对于Text('Parent: incr counter[0].counter')的onClick事件,this.counter[0].incrSubCounter(10)调用incrSubCounter方法使SubCounter的counter值增加10,UI同步刷新。
1392
1393但是,在Text('Parent: set.counter to 10')的onClick中调用this.counter[0].setSubCounter(10),SubCounter的counter值却无法重置为10。
1394
1395incrSubCounter和setSubCounter都是同一个SubCounter的函数。在第一个点击处理时调用incrSubCounter可以正确更新UI,而第二个点击处理调用setSubCounter时却没有更新UI。实际上incrSubCounter和setSubCounter两个函数都不能触发Text('${this.value.subCounter.counter}')的更新,因为\@ObjectLink value : ParentCounter仅能观察其代理ParentCounter的属性,对于this.value.subCounter.counter是SubCounter的属性,无法观察到嵌套类的属性。
1396
1397但是,第一个click事件调用this.counter[0].incrCounter()将CounterComp自定义组件中\@ObjectLink value: ParentCounter标记为已更改。此时触发Text('${this.value.subCounter.counter}')的更新。 如果在第一个点击事件中删除this.counter[0].incrCounter(),也无法更新UI。
1398
1399【正例】
1400
1401对于上述问题,为了直接观察SubCounter中的属性,以便this.counter[0].setSubCounter(10)操作有效,可以利用下面的方法:
1402
1403
1404```ts
1405CounterComp({ value: this.counter[0] }); // ParentComp组件传递 ParentCounter 给 CounterComp 组件
1406@ObjectLink value:ParentCounter; // @ObjectLink 接收 ParentCounter
1407
1408CounterChild({ subValue: this.value.subCounter }); // CounterComp组件 传递 SubCounter 给 CounterChild 组件
1409@ObjectLink subValue:SubCounter; // @ObjectLink 接收 SubCounter
1410```
1411
1412该方法使得\@ObjectLink分别代理了ParentCounter和SubCounter的属性,这样对于这两个类的属性的变化都可以观察到,即都会对UI视图进行刷新。即使删除了上面所说的this.counter[0].incrCounter(),UI也会进行正确的刷新。
1413
1414该方法可用于实现“两个层级”的观察,即外部对象和内部嵌套对象的观察。但是该方法只能用于\@ObjectLink装饰器,无法作用于\@Prop(\@Prop通过深拷贝传入对象)。详情参考[@Prop与@ObjectLink的差异](#prop与objectlink的差异)。
1415
1416
1417```ts
1418let nextId = 1;
1419
1420@Observed
1421class SubCounter {
1422  counter: number;
1423
1424  constructor(c: number) {
1425    this.counter = c;
1426  }
1427}
1428
1429@Observed
1430class ParentCounter {
1431  id: number;
1432  counter: number;
1433  subCounter: SubCounter;
1434
1435  incrCounter() {
1436    this.counter++;
1437  }
1438
1439  incrSubCounter(c: number) {
1440    this.subCounter.counter += c;
1441  }
1442
1443  setSubCounter(c: number): void {
1444    this.subCounter.counter = c;
1445  }
1446
1447  constructor(c: number) {
1448    this.id = nextId++;
1449    this.counter = c;
1450    this.subCounter = new SubCounter(c);
1451  }
1452}
1453
1454@Component
1455struct CounterComp {
1456  @ObjectLink value: ParentCounter;
1457
1458  build() {
1459    Column({ space: 10 }) {
1460      Text(`${this.value.counter}`)
1461        .fontSize(25)
1462        .onClick(() => {
1463          this.value.incrCounter();
1464        })
1465      CounterChild({ subValue: this.value.subCounter })
1466      Divider().height(2)
1467    }
1468  }
1469}
1470
1471@Component
1472struct CounterChild {
1473  @ObjectLink subValue: SubCounter;
1474
1475  build() {
1476    Text(`${this.subValue.counter}`)
1477      .onClick(() => {
1478        this.subValue.counter += 1;
1479      })
1480  }
1481}
1482
1483@Entry
1484@Component
1485struct ParentComp {
1486  @State counter: ParentCounter[] = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)];
1487
1488  build() {
1489    Row() {
1490      Column() {
1491        CounterComp({ value: this.counter[0] })
1492        CounterComp({ value: this.counter[1] })
1493        CounterComp({ value: this.counter[2] })
1494        Divider().height(5)
1495        ForEach(this.counter,
1496          (item: ParentCounter) => {
1497            CounterComp({ value: item })
1498          },
1499          (item: ParentCounter) => item.id.toString()
1500        )
1501        Divider().height(5)
1502        Text('Parent: reset entire counter')
1503          .fontSize(20).height(50)
1504          .onClick(() => {
1505            this.counter = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)];
1506          })
1507        Text('Parent: incr counter[0].counter')
1508          .fontSize(20).height(50)
1509          .onClick(() => {
1510            this.counter[0].incrCounter();
1511            this.counter[0].incrSubCounter(10);
1512          })
1513        Text('Parent: set.counter to 10')
1514          .fontSize(20).height(50)
1515          .onClick(() => {
1516            this.counter[0].setSubCounter(10);
1517          })
1518      }
1519    }
1520  }
1521}
1522```
1523
1524### \@Prop与\@ObjectLink的差异
1525
1526在下面的示例代码中,\@ObjectLink装饰的变量是对数据源的引用,即this.value.subCounterthis.subValue都是同一个对象的不同引用,所以在点击CounterComp的click handler,改变this.value.subCounter.counter时,this.subValue.counter也会改变,对应的组件Text(`this.subValue.counter: ${this.subValue.counter}`)会刷新。
1527
1528
1529```ts
1530let nextId = 1;
1531
1532@Observed
1533class SubCounter {
1534  counter: number;
1535
1536  constructor(c: number) {
1537    this.counter = c;
1538  }
1539}
1540
1541@Observed
1542class ParentCounter {
1543  id: number;
1544  counter: number;
1545  subCounter: SubCounter;
1546
1547  incrCounter() {
1548    this.counter++;
1549  }
1550
1551  incrSubCounter(c: number) {
1552    this.subCounter.counter += c;
1553  }
1554
1555  setSubCounter(c: number): void {
1556    this.subCounter.counter = c;
1557  }
1558
1559  constructor(c: number) {
1560    this.id = nextId++;
1561    this.counter = c;
1562    this.subCounter = new SubCounter(c);
1563  }
1564}
1565
1566@Component
1567struct CounterComp {
1568  @ObjectLink value: ParentCounter;
1569
1570  build() {
1571    Column({ space: 10 }) {
1572      CountChild({ subValue: this.value.subCounter })
1573      Text(`this.value.counter:increase 7 `)
1574        .fontSize(30)
1575        .onClick(() => {
1576          // 点击后Text(`this.subValue.counter: ${this.subValue.counter}`)会刷新
1577          this.value.incrSubCounter(7);
1578        })
1579      Divider().height(2)
1580    }
1581  }
1582}
1583
1584@Component
1585struct CountChild {
1586  @ObjectLink subValue: SubCounter;
1587
1588  build() {
1589    Text(`this.subValue.counter: ${this.subValue.counter}`)
1590      .fontSize(30)
1591  }
1592}
1593
1594@Entry
1595@Component
1596struct ParentComp {
1597  @State counter: ParentCounter[] = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)];
1598
1599  build() {
1600    Row() {
1601      Column() {
1602        CounterComp({ value: this.counter[0] })
1603        CounterComp({ value: this.counter[1] })
1604        CounterComp({ value: this.counter[2] })
1605        Divider().height(5)
1606        ForEach(this.counter,
1607          (item: ParentCounter) => {
1608            CounterComp({ value: item })
1609          },
1610          (item: ParentCounter) => item.id.toString()
1611        )
1612        Divider().height(5)
1613        Text('Parent: reset entire counter')
1614          .fontSize(20).height(50)
1615          .onClick(() => {
1616            this.counter = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)];
1617          })
1618        Text('Parent: incr counter[0].counter')
1619          .fontSize(20).height(50)
1620          .onClick(() => {
1621            this.counter[0].incrCounter();
1622            this.counter[0].incrSubCounter(10);
1623          })
1624        Text('Parent: set.counter to 10')
1625          .fontSize(20).height(50)
1626          .onClick(() => {
1627            this.counter[0].setSubCounter(10);
1628          })
1629      }
1630    }
1631  }
1632}
1633```
1634
1635\@ObjectLink图示如下:
1636
1637![zh-cn_image_0000001651665921](figures/zh-cn_image_0000001651665921.png)
1638
1639【反例】
1640
1641如果用\@Prop替代\@ObjectLink。点击Text(`this.subValue.counter: ${this.subValue.counter}`),UI刷新正常。但是点击Text(`this.value.counter:increase 7 `),\@Prop 对变量做了一个本地拷贝,CounterComp的第一个Text并不会刷新。
1642
1643  this.value.subCounterthis.subValue并不是同一个对象。所以this.value.subCounter的改变,并没有改变this.subValue的拷贝对象,Text(`this.subValue.counter: ${this.subValue.counter}`)不会刷新。
1644
1645```ts
1646@Component
1647struct CounterComp {
1648  @Prop value: ParentCounter = new ParentCounter(0);
1649  @Prop subValue: SubCounter = new SubCounter(0);
1650  build() {
1651    Column({ space: 10 }) {
1652      Text(`this.subValue.counter: ${this.subValue.counter}`)
1653        .fontSize(20)
1654        .onClick(() => {
1655          this.subValue.counter += 7;
1656        })
1657      Text(`this.value.counter:increase 7 `)
1658        .fontSize(20)
1659        .onClick(() => {
1660          this.value.incrSubCounter(7);
1661        })
1662      Divider().height(2)
1663    }
1664  }
1665}
1666```
1667
1668\@Prop拷贝的关系图示如下:
1669
1670![zh-cn_image_0000001602146116](figures/zh-cn_image_0000001602146116.png)
1671
1672【正例】
1673
1674可以通过从ParentComp到CounterComp仅拷贝一份\@Prop value: ParentCounter,同时必须避免再多拷贝一份SubCounter。
1675
1676- 在CounterComp组件中只使用一个\@Prop counter:Counter。
1677
1678- 添加另一个子组件SubCounterComp,其中包含\@ObjectLink subCounter: SubCounter。此\@ObjectLink可确保观察到SubCounter对象属性更改,并且UI更新正常。
1679
1680- \@ObjectLink subCounter: SubCounter与CounterComp中的\@Prop counter:Counter的this.counter.subCounter共享相同的SubCounter对象。
1681
1682
1683
1684```ts
1685let nextId = 1;
1686
1687@Observed
1688class SubCounter {
1689  counter: number;
1690  constructor(c: number) {
1691    this.counter = c;
1692  }
1693}
1694
1695@Observed
1696class ParentCounter {
1697  id: number;
1698  counter: number;
1699  subCounter: SubCounter;
1700  incrCounter() {
1701    this.counter++;
1702  }
1703  incrSubCounter(c: number) {
1704    this.subCounter.counter += c;
1705  }
1706  setSubCounter(c: number): void {
1707    this.subCounter.counter = c;
1708  }
1709  constructor(c: number) {
1710    this.id = nextId++;
1711    this.counter = c;
1712    this.subCounter = new SubCounter(c);
1713  }
1714}
1715
1716@Component
1717struct SubCounterComp {
1718  @ObjectLink subValue: SubCounter;
1719  build() {
1720    Text(`SubCounterComp: this.subValue.counter: ${this.subValue.counter}`)
1721      .onClick(() => {
1722        this.subValue.counter = 7;
1723      })
1724  }
1725}
1726@Component
1727struct CounterComp {
1728  @Prop value: ParentCounter;
1729  build() {
1730    Column({ space: 10 }) {
1731      Text(`this.value.incrCounter(): this.value.counter: ${this.value.counter}`)
1732        .fontSize(20)
1733        .onClick(() => {
1734          this.value.incrCounter();
1735        })
1736      SubCounterComp({ subValue: this.value.subCounter })
1737      Text(`this.value.incrSubCounter()`)
1738        .onClick(() => {
1739          this.value.incrSubCounter(77);
1740        })
1741      Divider().height(2)
1742    }
1743  }
1744}
1745@Entry
1746@Component
1747struct ParentComp {
1748  @State counter: ParentCounter[] = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)];
1749  build() {
1750    Row() {
1751      Column() {
1752        CounterComp({ value: this.counter[0] })
1753        CounterComp({ value: this.counter[1] })
1754        CounterComp({ value: this.counter[2] })
1755        Divider().height(5)
1756        ForEach(this.counter,
1757          (item: ParentCounter) => {
1758            CounterComp({ value: item })
1759          },
1760          (item: ParentCounter) => item.id.toString()
1761        )
1762        Divider().height(5)
1763        Text('Parent: reset entire counter')
1764          .fontSize(20).height(50)
1765          .onClick(() => {
1766            this.counter = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)];
1767          })
1768        Text('Parent: incr counter[0].counter')
1769          .fontSize(20).height(50)
1770          .onClick(() => {
1771            this.counter[0].incrCounter();
1772            this.counter[0].incrSubCounter(10);
1773          })
1774        Text('Parent: set.counter to 10')
1775          .fontSize(20).height(50)
1776          .onClick(() => {
1777            this.counter[0].setSubCounter(10);
1778          })
1779      }
1780    }
1781  }
1782}
1783```
1784
1785
1786拷贝关系图示如下:
1787
1788
1789![zh-cn_image_0000001653949465](figures/zh-cn_image_0000001653949465.png)
1790
1791### 在@Observed装饰类的构造函数中延时更改成员变量
1792
1793在状态管理中,使用@Observed装饰类后,会给该类使用一层“代理”进行包装。当在组件中改变该类的成员变量时,会被该代理进行拦截,在更改数据源中值的同时,也会将变化通知给绑定的组件,从而实现观测变化与触发刷新。
1794
1795当开发者在类的构造函数中对成员变量进行赋值或者修改时,此修改不会经过代理(因为是直接对数据源中的值进行修改),也就无法被观测到。所以,如果开发者在类的构造函数中使用定时器修改类中的成员变量,即使该修改成功执行了,也不会触发UI的刷新。
1796
1797【反例】
1798
1799```ts
1800@Observed
1801class RenderClass {
1802  waitToRender: boolean = false;
1803
1804  constructor() {
1805    setTimeout(() => {
1806      this.waitToRender = true;
1807      console.log("更改waitToRender的值为:" + this.waitToRender);
1808    }, 1000)
1809  }
1810}
1811
1812@Entry
1813@Component
1814struct Index {
1815  @State @Watch('renderClassChange') renderClass: RenderClass = new RenderClass();
1816  @State textColor: Color = Color.Black;
1817
1818  renderClassChange() {
1819    console.log("renderClass的值被更改为:" + this.renderClass.waitToRender);
1820  }
1821
1822  build() {
1823    Row() {
1824      Column() {
1825        Text("renderClass的值为:" + this.renderClass.waitToRender)
1826          .fontSize(20)
1827          .fontColor(this.textColor)
1828        Button("Show")
1829          .onClick(() => {
1830            // 使用其他状态变量强行刷新UI的做法并不推荐,此处仅用来检测waitToRender的值是否更新
1831            this.textColor = Color.Red;
1832          })
1833      }
1834      .width('100%')
1835    }
1836    .height('100%')
1837  }
1838}
1839```
1840
1841上文的示例代码中在RenderClass的构造函数中使用定时器在1秒后修改了waitToRender的值,但是不会触发UI的刷新。此时点击按钮,强行刷新Text组件可以看到waitToRender的值已经被修改成了true。
1842
1843【正例】
1844
1845```ts
1846@Observed
1847class RenderClass {
1848  waitToRender: boolean = false;
1849
1850  constructor() {
1851  }
1852}
1853
1854@Entry
1855@Component
1856struct Index {
1857  @State @Watch('renderClassChange') renderClass: RenderClass = new RenderClass();
1858
1859  renderClassChange() {
1860    console.log("renderClass的值被更改为:" + this.renderClass.waitToRender);
1861  }
1862
1863  onPageShow() {
1864    setTimeout(() => {
1865      this.renderClass.waitToRender = true;
1866      console.log("更改renderClass的值为:" + this.renderClass.waitToRender);
1867    }, 1000)
1868  }
1869
1870  build() {
1871    Row() {
1872      Column() {
1873        Text("renderClass的值为:" + this.renderClass.waitToRender)
1874          .fontSize(20)
1875      }
1876      .width('100%')
1877    }
1878    .height('100%')
1879  }
1880}
1881```
1882
1883上文的示例代码将定时器修改移入到组件内,此时界面显示时会先显示“renderClass的值为:false”。待定时器触发时,renderClass的值改变,触发[@Watch](./arkts-watch.md)回调,此时界面刷新显示“renderClass的值为:true”,日志输出“renderClass的值被更改为:true”。
1884
1885因此,更推荐开发者在组件中对@Observed装饰的类成员变量进行修改实现刷新。
1886
1887### \@ObjectLink数据源更新时机
1888
1889```ts
1890@Observed
1891class Person {
1892  name: string = '';
1893  age: number = 0;
1894
1895  constructor(name: string, age: number) {
1896    this.name = name;
1897    this.age = age;
1898  }
1899}
1900
1901@Observed
1902class Info {
1903  person: Person;
1904
1905  constructor(person: Person) {
1906    this.person = person;
1907  }
1908}
1909
1910@Entry
1911@Component
1912struct Parent {
1913  @State @Watch('onChange01') info: Info = new Info(new Person('Bob', 10));
1914
1915  onChange01() {
1916    console.log(':::onChange01:' + this.info.person.name); // 2
1917  }
1918
1919  build() {
1920    Column() {
1921      Text(this.info.person.name).height(40)
1922      Child({
1923        per: this.info.person, clickEvent: () => {
1924          console.log(':::clickEvent before', this.info.person.name); // 1
1925          this.info.person = new Person('Jack', 12);
1926          console.log(':::clickEvent after', this.info.person.name); // 3
1927        }
1928      })
1929    }
1930  }
1931}
1932
1933@Component
1934struct Child {
1935  @ObjectLink @Watch('onChange02') per: Person;
1936  clickEvent?: () => void;
1937
1938  onChange02() {
1939    console.log(':::onChange02:' + this.per.name); // 5
1940  }
1941
1942  build() {
1943    Column() {
1944      Button(this.per.name)
1945        .height(40)
1946        .onClick(() => {
1947          this.onClickType();
1948        })
1949    }
1950  }
1951
1952  private onClickType() {
1953    if (this.clickEvent) {
1954      this.clickEvent();
1955    }
1956    console.log(':::--------此时Child中的this.per.name值仍然是:' + this.per.name); // 4
1957  }
1958}
1959```
1960
1961\@ObjectLink的数据源更新依赖其父组件,当父组件中数据源改变引起父组件刷新时,会重新设置子组件\@ObjectLink的数据源。这个过程不是在父组件数据源变化后立刻发生的,而是在父组件实际刷新时才会进行。上述示例中,Parent包含Child,Parent传递箭头函数给Child,在点击时,日志打印顺序是1-2-3-4-5,打印到日志4时,点击事件流程结束,此时仅仅是将子组件Child标记为需要父组件更新的节点,因此日志4打印的this.per.name的值仍为Bob,等到父组件真正更新时,才会更新Child的数据源。
1962
1963当@ObjectLink @Watch('onChange02') per: Person的\@Watch函数执行时,说明\@ObjectLink的数据源已被父组件更新,此时日志5打印的值为更新后的Jack。
1964
1965日志的含义为:
1966- 日志1:对Parent @State @Watch('onChange01') info: Info = new Info(new Person('Bob', 10)) 赋值前。
1967
1968- 日志2:对Parent @State @Watch('onChange01') info: Info = new Info(new Person('Bob', 10)) 赋值,执行其\@Watch函数,同步执行。
1969
1970- 日志3:对Parent @State @Watch('onChange01') info: Info = new Info(new Person('Bob', 10)) 赋值完成。
1971
1972- 日志4:onClickType方法内clickEvent执行完,此时只是将子组件Child标记为需要父组件更新的节点,未将最新的值更新给Child @ObjectLink @Watch('onChange02') per: Person,所以日志4打印的this.per.name的值仍然是Bob。
1973
1974- 日志5:下一次vsync信号触发Child更新,@ObjectLink @Watch('onChange02') per: Person被更新,触发其\@Watch方法,此时@ObjectLink @Watch('onChange02') per: Person为新值Jack。
1975
1976\@Prop父子同步原理同\@ObjectLink一致。
1977
1978当clickEvent中更改this.info.person.name时,修改会立刻生效,此时日志4打印的值是Jack。
1979
1980```ts
1981Child({
1982  per: this.info.person, clickEvent: () => {
1983    console.log(':::clickEvent before', this.info.person.name); // 1
1984    this.info.person.name = 'Jack';
1985    console.log(':::clickEvent after', this.info.person.name); // 3
1986  }
1987})
1988```
1989
1990此时Parent中Text组件不会刷新,因为this.info.person.name属于两层嵌套。
1991
1992### 使用a.b(this.object)形式调用,不会触发UI刷新
1993
1994在build方法内,当@Observed与@ObjectLink联合装饰的变量是Object类型、且通过a.b(this.object)形式调用时,b方法内传入的是this.object的原生对象,修改其属性,无法触发UI刷新。如下例中,通过静态方法或者使用this调用组件内部方法,修改组件中的this.weather.temperature时,UI不会刷新。
1995
1996【反例】
1997
1998```ts
1999@Observed
2000class Weather {
2001  temperature:number;
2002
2003  constructor(temperature:number) {
2004    this.temperature = temperature;
2005  }
2006
2007  static increaseTemperature(weather:Weather) {
2008    weather.temperature++;
2009  }
2010}
2011
2012class Day {
2013  weather:Weather;
2014  week:string;
2015  constructor(weather:Weather, week:string) {
2016    this.weather = weather;
2017    this.week = week;
2018  }
2019}
2020
2021@Entry
2022@Component
2023struct Parent {
2024  @State day1: Day = new Day(new Weather(15), 'Monday');
2025
2026  build() {
2027    Column({ space:10 }) {
2028      Child({ weather: this.day1.weather})
2029    }
2030    .height('100%')
2031    .width('100%')
2032  }
2033}
2034
2035@Component
2036struct Child {
2037  @ObjectLink weather: Weather;
2038
2039  reduceTemperature (weather:Weather) {
2040    weather.temperature--;
2041  }
2042
2043  build() {
2044    Column({ space:10 }) {
2045      Text(`The temperature of day1 is ${this.weather.temperature} degrees.`)
2046        .fontSize(20)
2047      Button('increaseTemperature')
2048        .onClick(()=>{
2049          // 通过静态方法调用,无法触发UI刷新
2050          Weather.increaseTemperature(this.weather);
2051        })
2052      Button('reduceTemperature')
2053        .onClick(()=>{
2054          // 使用this通过自定义组件内部方法调用,无法触发UI刷新
2055          this.reduceTemperature(this.weather);
2056        })
2057    }
2058    .height('100%')
2059    .width('100%')
2060  }
2061}
2062```
2063
2064可以通过如下先赋值、再调用新赋值的变量的方式为this.weather加上Proxy代理,实现UI刷新。
2065
2066【正例】
2067
2068```ts
2069@Observed
2070class Weather {
2071  temperature:number;
2072
2073  constructor(temperature:number) {
2074    this.temperature = temperature;
2075  }
2076
2077  static increaseTemperature(weather:Weather) {
2078    weather.temperature++;
2079  }
2080}
2081
2082class Day {
2083  weather:Weather;
2084  week:string;
2085  constructor(weather:Weather, week:string) {
2086    this.weather = weather;
2087    this.week = week;
2088  }
2089}
2090
2091@Entry
2092@Component
2093struct Parent {
2094  @State day1: Day = new Day(new Weather(15), 'Monday');
2095
2096  build() {
2097    Column({ space:10 }) {
2098      Child({ weather: this.day1.weather})
2099    }
2100    .height('100%')
2101    .width('100%')
2102  }
2103}
2104
2105@Component
2106struct Child {
2107  @ObjectLink weather: Weather;
2108
2109  reduceTemperature (weather:Weather) {
2110    weather.temperature--;
2111  }
2112
2113  build() {
2114    Column({ space:10 }) {
2115      Text(`The temperature of day1 is ${this.weather.temperature} degrees.`)
2116        .fontSize(20)
2117      Button('increaseTemperature')
2118        .onClick(()=>{
2119          // 通过赋值添加 Proxy 代理
2120          let weather1 = this.weather;
2121          Weather.increaseTemperature(weather1);
2122        })
2123      Button('reduceTemperature')
2124        .onClick(()=>{
2125          // 通过赋值添加 Proxy 代理
2126          let weather2 = this.weather;
2127          this.reduceTemperature(weather2);
2128        })
2129    }
2130    .height('100%')
2131    .width('100%')
2132  }
2133}
2134```
2135