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