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