1# \@ObservedV2装饰器和\@Trace装饰器:类属性变化观测
2
3为了增强状态管理框架对类对象中属性的观测能力,开发者可以使用\@ObservedV2装饰器和\@Trace装饰器装饰类以及类中的属性。
4
5
6\@ObservedV2和\@Trace提供了对嵌套类对象属性变化直接观测的能力,是状态管理V2中相对核心的能力之一。在阅读本文当前,建议提前阅读:[状态管理概述](./arkts-state-management-overview.md)来了解状态管理V2整体的能力架构。
7
8>**说明:**
9>
10>\@ObservedV2与\@Trace装饰器从API version 12开始支持。
11>
12
13## 概述
14
15\@ObservedV2装饰器与\@Trace装饰器用于装饰类以及类中的属性,使得被装饰的类和属性具有深度观测的能力:
16
17- \@ObservedV2装饰器与\@Trace装饰器需要配合使用,单独使用\@ObservedV2装饰器或\@Trace装饰器没有任何作用。
18- 被\@Trace装饰器装饰的属性property变化时,仅会通知property关联的组件进行刷新。
19- 在嵌套类中,嵌套类中的属性property被\@Trace装饰且嵌套类被\@ObservedV2装饰时,才具有触发UI刷新的能力。
20- 在继承类中,父类或子类中的属性property被\@Trace装饰且该property所在类被\@ObservedV2装饰时,才具有触发UI刷新的能力。
21- 未被\@Trace装饰的属性用在UI中无法感知到变化,也无法触发UI刷新。
22- \@ObservedV2的类实例目前不支持使用JSON.stringify进行序列化。
23
24## 状态管理V1版本对嵌套类对象属性变化直接观测的局限性
25
26现有状态管理V1版本无法实现对嵌套类对象属性变化的直接观测。
27
28```ts
29@Observed
30class Father {
31  son: Son;
32
33  constructor(name: string, age: number) {
34    this.son = new Son(name, age);
35  }
36}
37@Observed
38class Son {
39  name: string;
40  age: number;
41
42  constructor(name: string, age: number) {
43    this.name = name;
44    this.age = age;
45  }
46}
47@Entry
48@Component
49struct Index {
50  @State father: Father = new Father("John", 8);
51
52  build() {
53    Row() {
54      Column() {
55        Text(`name: ${this.father.son.name} age: ${this.father.son.age}`)
56          .fontSize(50)
57          .fontWeight(FontWeight.Bold)
58          .onClick(() => {
59            this.father.son.age++;
60          })
61      }
62      .width('100%')
63    }
64    .height('100%')
65  }
66}
67```
68
69上述代码中,点击Text组件增加age的值时,不会触发UI刷新。因为在现有的状态管理框架下,无法观测到嵌套类中属性age的值变化。V1版本的解决方案是使用[\@ObjectLink装饰器](arkts-observed-and-objectlink.md)与自定义组件的方式实现观测。
70
71```ts
72@Observed
73class Father {
74  son: Son;
75
76  constructor(name: string, age: number) {
77    this.son = new Son(name, age);
78  }
79}
80@Observed
81class Son {
82  name: string;
83  age: number;
84
85  constructor(name: string, age: number) {
86    this.name = name;
87    this.age = age;
88  }
89}
90@Component
91struct Child {
92  @ObjectLink son: Son;
93
94  build() {
95    Row() {
96      Column() {
97        Text(`name: ${this.son.name} age: ${this.son.age}`)
98          .fontSize(50)
99          .fontWeight(FontWeight.Bold)
100          .onClick(() => {
101            this.son.age++;
102          })
103      }
104      .width('100%')
105    }
106    .height('100%')
107  }
108}
109@Entry
110@Component
111struct Index {
112  @State father: Father = new Father("John", 8);
113
114  build() {
115    Column() {
116      Child({son: this.father.son})
117    }
118  }
119}
120```
121
122通过这种方式虽然能够实现对嵌套类中属性变化的观测,但是当嵌套层级较深时,代码将会变得十分复杂,易用性差。因此推出类装饰器\@ObservedV2与成员变量装饰器\@Trace,增强对嵌套类中属性变化的观测能力。
123
124## 装饰器说明
125
126| \@ObservedV2类装饰器 | 说明                                                  |
127| ------------------ | ----------------------------------------------------- |
128| 装饰器参数         | 无                                                    |
129| 类装饰器           | 装饰class。需要放在class的定义前,使用new创建类对象。 |
130
131| \@Trace成员变量装饰器 | 说明                                                         |
132| --------------------- | ------------------------------------------------------------ |
133| 装饰器参数            | 无                                                           |
134| 可装饰的变量          | class中成员属性。属性的类型可以为number、string、boolean、class、Array、Date、Map、Set等类型。 |
135
136## 观察变化
137
138使用\@ObservedV2装饰的类中被\@Trace装饰的属性具有被观测变化的能力,当该属性值变化时,会触发该属性绑定的UI组件刷新。
139
140- 在嵌套类中使用\@Trace装饰的属性具有被观测变化的能力。
141
142```ts
143@ObservedV2
144class Son {
145  @Trace age: number = 100;
146}
147class Father {
148  son: Son = new Son();
149}
150@Entry
151@ComponentV2
152struct Index {
153  father: Father = new Father();
154
155  build() {
156    Column() {
157      // 当点击改变age时,Text组件会刷新
158      Text(`${this.father.son.age}`)
159        .onClick(() => {
160          this.father.son.age++;
161        })
162    }
163  }
164}
165
166```
167
168- 在继承类中使用\@Trace装饰的属性具有被观测变化的能力。
169
170```ts
171@ObservedV2
172class Father {
173  @Trace name: string = "Tom";
174}
175class Son extends Father {
176}
177@Entry
178@ComponentV2
179struct Index {
180  son: Son = new Son();
181
182  build() {
183    Column() {
184      // 当点击改变name时,Text组件会刷新
185      Text(`${this.son.name}`)
186        .onClick(() => {
187          this.son.name = "Jack";
188        })
189    }
190  }
191}
192```
193
194- 类中使用\@Trace装饰的静态属性具有被观测变化的能力。
195
196```ts
197@ObservedV2
198class Manager {
199  @Trace static count: number = 1;
200}
201@Entry
202@ComponentV2
203struct Index {
204  build() {
205    Column() {
206      // 当点击改变count时,Text组件会刷新
207      Text(`${Manager.count}`)
208        .onClick(() => {
209          Manager.count++;
210        })
211    }
212  }
213}
214```
215
216- \@Trace装饰内置类型时,可以观测各自API导致的变化:
217
218  | 类型  | 可观测变化的API                                              |
219  | ----- | ------------------------------------------------------------ |
220  | Array | push、pop、shift、unshift、splice、copyWithin、fill、reverse、sort |
221  | Date  | setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds |
222  | Map   | set, clear, delete                                           |
223  | Set   | add, clear, delete                                           |
224
225## 使用限制
226
227\@ObservedV2与\@Trace装饰器存在以下使用限制:
228
229- 非\@Trace装饰的成员属性用在UI上无法触发UI刷新。
230
231```ts
232@ObservedV2
233class Person {
234  id: number = 0;
235  @Trace age: number = 8;
236}
237@Entry
238@ComponentV2
239struct Index {
240  person: Person = new Person();
241
242  build() {
243    Column() {
244      // age被@Trace装饰,用在UI中可以触发UI刷新
245      Text(`${this.person.age}`)
246        .onClick(() => {
247          this.person.age++; // 点击会触发UI刷新
248        })
249      // id未被@Trace装饰,用在UI中不会触发UI刷新
250      Text(`${this.person.id}`) // 当id变化时不会刷新
251        .onClick(() => {
252          this.person.id++; // 点击不会触发UI刷新
253        })
254    }
255  }
256}
257```
258
259- \@ObservedV2仅能装饰class,无法装饰自定义组件。
260
261```ts
262@ObservedV2 // 错误用法,编译时报错
263struct Index {
264  build() {
265  }
266}
267```
268
269- \@Trace不能用在没有被\@ObservedV2装饰的class上。
270
271```ts
272class User {
273  id: number = 0;
274  @Trace name: string = "Tom"; // 错误用法,编译时报错
275}
276```
277
278- \@Trace是class中属性的装饰器,不能用在struct中。
279
280```ts
281@ComponentV2
282struct Comp {
283  @Trace message: string = "Hello World"; // 错误用法,编译时报错
284
285  build() {
286  }
287}
288```
289
290- \@ObservedV2、\@Trace不能与[\@Observed](arkts-observed-and-objectlink.md)、[\@Track](arkts-track.md)混合使用。
291
292```ts
293@Observed
294class User {
295  @Trace name: string = "Tom"; // 错误用法,编译时报错
296}
297
298@ObservedV2
299class Person {
300  @Track name: string = "Jack"; // 错误用法,编译时报错
301}
302```
303
304- 使用\@ObservedV2与\@Trace装饰的类不能和[\@State](arkts-state.md)等V1的装饰器混合使用,编译时报错。
305
306```ts
307// 以@State装饰器为例
308@ObservedV2
309class Job {
310  @Trace jobName: string = "Teacher";
311}
312@ObservedV2
313class Info {
314  @Trace name: string = "Tom";
315  @Trace age: number = 25;
316  job: Job = new Job();
317}
318@Entry
319@Component
320struct Index {
321  @State info: Info = new Info(); // 无法混用,编译时报错
322
323  build() {
324    Column() {
325      Text(`name: ${this.info.name}`)
326      Text(`age: ${this.info.age}`)
327      Text(`jobName: ${this.info.job.jobName}`)
328      Button("change age")
329        .onClick(() => {
330          this.info.age++;
331        })
332      Button("Change job")
333        .onClick(() => {
334          this.info.job.jobName = "Doctor";
335        })
336    }
337  }
338}
339```
340
341- 继承自\@ObservedV2的类无法和\@State等V1的装饰器混用,运行时报错。
342
343```ts
344// 以@State装饰器为例
345@ObservedV2
346class Job {
347  @Trace jobName: string = "Teacher";
348}
349@ObservedV2
350class Info {
351  @Trace name: string = "Tom";
352  @Trace age: number = 25;
353  job: Job = new Job();
354}
355class Message extends Info {
356    constructor() {
357        super();
358    }
359}
360@Entry
361@Component
362struct Index {
363  @State message: Message = new Message(); // 无法混用,运行时报错
364
365  build() {
366    Column() {
367      Text(`name: ${this.message.name}`)
368      Text(`age: ${this.message.age}`)
369      Text(`jobName: ${this.message.job.jobName}`)
370      Button("change age")
371        .onClick(() => {
372          this.message.age++;
373        })
374      Button("Change job")
375        .onClick(() => {
376          this.message.job.jobName = "Doctor";
377        })
378    }
379  }
380}
381```
382
383- \@ObservedV2的类实例目前不支持使用JSON.stringify进行序列化。
384
385## 使用场景
386
387### 嵌套类场景
388
389在下面的嵌套类场景中,Pencil类是Son类中最里层的类,Pencil类被\@ObservedV2装饰且属性length被\@Trace装饰,此时length的变化能够被观测到。
390
391\@Trace装饰器与现有状态管理框架的[\@Track](arkts-track.md)与[\@State](arkts-state.md)装饰器的能力不同,@Track使class具有属性级更新的能力,但并不具备深度观测的能力;而\@State只能观测到对象本身以及第一层的变化,对于多层嵌套场景只能通过封装自定义组件,搭配[\@Observed](arkts-observed-and-objectlink.md)和[\@ObjectLink](arkts-observed-and-objectlink.md)来实现观测。
392
393* 点击Button("change length"),length是被\@Trace装饰的属性,它的变化可以触发关联的UI组件,即UINode (1)的刷新,并输出"id: 1 renderTimes: x"的日志,其中x根据点击次数依次增长。
394* 自定义组件Page中的son是常规变量,因此点击Button("assign Son")并不会观测到变化。
395* 当点击Button("assign Son")后,再点击Button("change length")并不会引起UI刷新。因为此时son的地址改变,其关联的UI组件并没有关联到最新的son。
396
397```ts
398@ObservedV2
399class Pencil {
400  @Trace length: number = 21; // 当length变化时,会刷新关联的组件
401}
402class Bag {
403  width: number = 50;
404  height: number = 60;
405  pencil: Pencil = new Pencil();
406}
407class Son {
408  age: number = 5;
409  school: string = "some";
410  bag: Bag = new Bag();
411}
412
413@Entry
414@ComponentV2
415struct Page {
416  son: Son = new Son();
417  renderTimes: number = 0;
418  isRender(id: number): number {
419    console.info(`id: ${id} renderTimes: ${this.renderTimes}`);
420    this.renderTimes++;
421    return 40;
422  }
423
424  build() {
425    Column() {
426      Text('pencil length'+ this.son.bag.pencil.length)
427        .fontSize(this.isRender(1))   // UINode (1)
428      Button("change length")
429        .onClick(() => {
430          // 点击更改length值,UINode(1)会刷新
431          this.son.bag.pencil.length += 100;
432        })
433      Button("assign Son")
434        .onClick(() => {
435          // 由于变量son非状态变量,因此无法刷新UINode(1)
436          this.son = new Son();
437        })
438    }
439  }
440}
441```
442
443
444### 继承类场景
445
446\@Trace支持在类的继承场景中使用,无论是在基类还是继承类中,只有被\@Trace装饰的属性才具有被观测变化的能力。
447以下例子中,声明class GrandFather、Father、Uncle、Son、Cousin,继承关系如下图。
448
449![arkts-old-state-management](figures/arkts-new-observed-and-track-extend-sample.png)
450
451
452创建类Son和类Cousin的实例,点击Button('change Son age')和Button('change Cousin age')可以触发UI的刷新。
453
454```ts
455@ObservedV2
456class GrandFather {
457  @Trace age: number = 0;
458
459  constructor(age: number) {
460    this.age = age;
461  }
462}
463class Father extends GrandFather{
464  constructor(father: number) {
465    super(father);
466  }
467}
468class Uncle extends GrandFather {
469  constructor(uncle: number) {
470    super(uncle);
471  }
472}
473class Son extends Father {
474  constructor(son: number) {
475    super(son);
476  }
477}
478class Cousin extends Uncle {
479  constructor(cousin: number) {
480    super(cousin);
481  }
482}
483@Entry
484@ComponentV2
485struct Index {
486  son: Son = new Son(0);
487  cousin: Cousin = new Cousin(0);
488  renderTimes: number = 0;
489
490  isRender(id: number): number {
491    console.info(`id: ${id} renderTimes: ${this.renderTimes}`);
492    this.renderTimes++;
493    return 40;
494  }
495
496  build() {
497    Row() {
498      Column() {
499        Text(`Son ${this.son.age}`)
500          .fontSize(this.isRender(1))
501          .fontWeight(FontWeight.Bold)
502        Text(`Cousin ${this.cousin.age}`)
503          .fontSize(this.isRender(2))
504          .fontWeight(FontWeight.Bold)
505        Button('change Son age')
506          .onClick(() => {
507            this.son.age++;
508          })
509        Button('change Cousin age')
510          .onClick(() => {
511            this.cousin.age++;
512          })
513      }
514      .width('100%')
515    }
516    .height('100%')
517  }
518}
519```
520
521### \@Trace装饰基础类型的数组
522
523\@Trace装饰数组时,使用支持的API能够观测到变化。支持的API见[观察变化](#观察变化)。
524在下面的示例中\@ObservedV2装饰的Arr类中的属性numberArr是\@Trace装饰的数组,当使用数组API操作numberArr时,可以观测到对应的变化。注意使用数组长度进行判断以防越界访问。
525
526```ts
527let nextId: number = 0;
528
529@ObservedV2
530class Arr {
531  id: number = 0;
532  @Trace numberArr: number[] = [];
533
534  constructor() {
535    this.id = nextId++;
536    this.numberArr = [0, 1, 2];
537  }
538}
539
540@Entry
541@ComponentV2
542struct Index {
543  arr: Arr = new Arr();
544
545  build() {
546    Column() {
547      Text(`length: ${this.arr.numberArr.length}`)
548        .fontSize(40)
549      Divider()
550      if (this.arr.numberArr.length >= 3) {
551        Text(`${this.arr.numberArr[0]}`)
552          .fontSize(40)
553          .onClick(() => {
554            this.arr.numberArr[0]++;
555          })
556        Text(`${this.arr.numberArr[1]}`)
557          .fontSize(40)
558          .onClick(() => {
559            this.arr.numberArr[1]++;
560          })
561        Text(`${this.arr.numberArr[2]}`)
562          .fontSize(40)
563          .onClick(() => {
564            this.arr.numberArr[2]++;
565          })
566      }
567
568      Divider()
569
570      ForEach(this.arr.numberArr, (item: number, index: number) => {
571        Text(`${index} ${item}`)
572          .fontSize(40)
573      })
574
575      Button('push')
576        .onClick(() => {
577          this.arr.numberArr.push(50);
578        })
579
580      Button('pop')
581        .onClick(() => {
582          this.arr.numberArr.pop();
583        })
584
585      Button('shift')
586        .onClick(() => {
587          this.arr.numberArr.shift();
588        })
589
590      Button('splice')
591        .onClick(() => {
592          this.arr.numberArr.splice(1, 0, 60);
593        })
594
595
596      Button('unshift')
597        .onClick(() => {
598          this.arr.numberArr.unshift(100);
599        })
600
601      Button('copywithin')
602        .onClick(() => {
603          this.arr.numberArr.copyWithin(0, 1, 2);
604        })
605
606      Button('fill')
607        .onClick(() => {
608          this.arr.numberArr.fill(0, 2, 4);
609        })
610
611      Button('reverse')
612        .onClick(() => {
613          this.arr.numberArr.reverse();
614        })
615
616      Button('sort')
617        .onClick(() => {
618          this.arr.numberArr.sort();
619        })
620    }
621  }
622}
623```
624
625### \@Trace装饰对象数组
626
627* \@Trace装饰对象数组personList以及Person类中的age属性,因此当personList、age改变时均可以观测到变化。
628* 点击Text组件更改age时,Text组件会刷新。
629
630```ts
631let nextId: number = 0;
632
633@ObservedV2
634class Person {
635  @Trace age: number = 0;
636
637  constructor(age: number) {
638    this.age = age;
639  }
640}
641
642@ObservedV2
643class Info {
644  id: number = 0;
645  @Trace personList: Person[] = [];
646
647  constructor() {
648    this.id = nextId++;
649    this.personList = [new Person(0), new Person(1), new Person(2)];
650  }
651}
652
653@Entry
654@ComponentV2
655struct Index {
656  info: Info = new Info();
657
658  build() {
659    Column() {
660      Text(`length: ${this.info.personList.length}`)
661        .fontSize(40)
662      Divider()
663      if (this.info.personList.length >= 3) {
664        Text(`${this.info.personList[0].age}`)
665          .fontSize(40)
666          .onClick(() => {
667            this.info.personList[0].age++;
668          })
669
670        Text(`${this.info.personList[1].age}`)
671          .fontSize(40)
672          .onClick(() => {
673            this.info.personList[1].age++;
674          })
675
676        Text(`${this.info.personList[2].age}`)
677          .fontSize(40)
678          .onClick(() => {
679            this.info.personList[2].age++;
680          })
681      }
682
683      Divider()
684
685      ForEach(this.info.personList, (item: Person, index: number) => {
686        Text(`${index} ${item.age}`)
687          .fontSize(40)
688      })
689    }
690  }
691}
692
693```
694
695### \@Trace装饰Map类型
696
697* 被\@Trace装饰的Map类型属性可以观测到调用API带来的变化,包括 set、clear、delete。
698* 因为Info类被\@ObservedV2装饰且属性memberMap被\@Trace装饰,点击Button('init map')对memberMap赋值也可以观测到变化。
699
700```ts
701@ObservedV2
702class Info {
703  @Trace memberMap: Map<number, string> = new Map([[0, "a"], [1, "b"], [3, "c"]]);
704}
705
706@Entry
707@ComponentV2
708struct MapSample {
709  info: Info = new Info();
710
711  build() {
712    Row() {
713      Column() {
714        ForEach(Array.from(this.info.memberMap.entries()), (item: [number, string]) => {
715          Text(`${item[0]}`)
716            .fontSize(30)
717          Text(`${item[1]}`)
718            .fontSize(30)
719          Divider()
720        })
721        Button('init map')
722          .onClick(() => {
723            this.info.memberMap = new Map([[0, "a"], [1, "b"], [3, "c"]]);
724          })
725        Button('set new one')
726          .onClick(() => {
727            this.info.memberMap.set(4, "d");
728          })
729        Button('clear')
730          .onClick(() => {
731            this.info.memberMap.clear();
732          })
733        Button('set the key: 0')
734          .onClick(() => {
735            this.info.memberMap.set(0, "aa");
736          })
737        Button('delete the first one')
738          .onClick(() => {
739            this.info.memberMap.delete(0);
740          })
741      }
742      .width('100%')
743    }
744    .height('100%')
745  }
746}
747```
748
749### \@Trace装饰Set类型
750
751* 被\@Trace装饰的Set类型属性可以观测到调用API带来的变化,包括 add, clear, delete。
752* 因为Info类被\@ObservedV2装饰且属性memberSet被\@Trace装饰,点击Button('init set')对memberSet赋值也可以观察变化。
753
754```ts
755@ObservedV2
756class Info {
757  @Trace memberSet: Set<number> = new Set([0, 1, 2, 3, 4]);
758}
759
760@Entry
761@ComponentV2
762struct SetSample {
763  info: Info = new Info();
764
765  build() {
766    Row() {
767      Column() {
768        ForEach(Array.from(this.info.memberSet.entries()), (item: [number, string]) => {
769          Text(`${item[0]}`)
770            .fontSize(30)
771          Divider()
772        })
773        Button('init set')
774          .onClick(() => {
775            this.info.memberSet = new Set([0, 1, 2, 3, 4]);
776          })
777        Button('set new one')
778          .onClick(() => {
779            this.info.memberSet.add(5);
780          })
781        Button('clear')
782          .onClick(() => {
783            this.info.memberSet.clear();
784          })
785        Button('delete the first one')
786          .onClick(() => {
787            this.info.memberSet.delete(0);
788          })
789      }
790      .width('100%')
791    }
792    .height('100%')
793  }
794}
795```
796
797
798### \@Trace装饰Date类型
799
800* \@Trace装饰的Date类型属性可以观测调用API带来的变化,包括 setFullYear、setMonth、setDate、setHours、setMinutes、setSeconds、setMilliseconds、setTime、setUTCFullYear、setUTCMonth、setUTCDate、setUTCHours、setUTCMinutes、setUTCSeconds、setUTCMilliseconds。
801* 因为Info类被\@ObservedV2装饰且属性selectedDate被\@Trace装饰,点击Button('set selectedDate to 2023-07-08')对selectedDate赋值也可以观测到变化。
802
803```ts
804@ObservedV2
805class Info {
806  @Trace selectedDate: Date = new Date('2021-08-08')
807}
808
809@Entry
810@ComponentV2
811struct DateSample {
812  info: Info = new Info()
813
814  build() {
815    Column() {
816      Button('set selectedDate to 2023-07-08')
817        .margin(10)
818        .onClick(() => {
819          this.info.selectedDate = new Date('2023-07-08');
820        })
821      Button('increase the year by 1')
822        .margin(10)
823        .onClick(() => {
824          this.info.selectedDate.setFullYear(this.info.selectedDate.getFullYear() + 1);
825        })
826      Button('increase the month by 1')
827        .margin(10)
828        .onClick(() => {
829          this.info.selectedDate.setMonth(this.info.selectedDate.getMonth() + 1);
830        })
831      Button('increase the day by 1')
832        .margin(10)
833        .onClick(() => {
834          this.info.selectedDate.setDate(this.info.selectedDate.getDate() + 1);
835        })
836      DatePicker({
837        start: new Date('1970-1-1'),
838        end: new Date('2100-1-1'),
839        selected: this.info.selectedDate
840      })
841    }.width('100%')
842  }
843}
844```
845