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 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