1# 状态管理优秀实践 2 3 4为了帮助应用程序开发人员提高其应用程序质量,特别是在高效的状态管理方面。本章节面向开发者提供了多个在开发ArkUI应用中常见场景和易错问题,并给出了对应的解决方案。此外,还提供了同一场景下,推荐用法和不推荐用法的对比和解释说明,更直观地展示两者区别,从而帮助开发者学习如果正确地在应用开发中使用状态变量,进行高性能开发。 5 6 7## 基础示例 8 9下面的例子是关于\@Prop,\@Link,\@ObjectLink的初始化规则的,在学习下面这个例子前,我们首先需要了解: 10 11- \@Prop:可以被父组件的\@State初始化,或者\@State是复杂类型Object和class时的属性,或者是数组时的数组项。 12 13- \@ObjectLink:初始化规则和\@Prop相同,但需要被\@Observed装饰class的实例初始化。 14 15- \@Link:必须和\@State或其他数据源类型完全相同。 16 17 18### 不推荐用法 19 20 21 22```ts 23@Observed 24class ClassA { 25 public c: number = 0; 26 27 constructor(c: number) { 28 this.c = c; 29 } 30} 31 32@Component 33struct LinkChild { 34 @Link testNum: number; 35 36 build() { 37 Text(`LinkChild testNum ${this.testNum}`) 38 } 39} 40 41 42@Component 43struct PropChild2 { 44 @Prop testNum: ClassA = new ClassA(0); 45 46 build() { 47 Text(`PropChild2 testNum ${this.testNum.c}`) 48 .onClick(() => { 49 this.testNum.c += 1; 50 }) 51 } 52} 53 54@Component 55struct PropChild3 { 56 @Prop testNum: ClassA = new ClassA(0); 57 58 build() { 59 Text(`PropChild3 testNum ${this.testNum.c}`) 60 } 61} 62 63@Component 64struct ObjectLinkChild { 65 @ObjectLink testNum: ClassA; 66 67 build() { 68 Text(`ObjectLinkChild testNum ${this.testNum.c}`) 69 .onClick(() => { 70 // 问题4:ObjectLink不能被赋值 71 this.testNum = new ClassA(47); 72 }) 73 } 74} 75 76@Entry 77@Component 78struct Parent { 79 @State testNum: ClassA[] = [new ClassA(1)]; 80 81 build() { 82 Column() { 83 Text(`Parent testNum ${this.testNum.c}`) 84 .onClick(() => { 85 this.testNum[0].c += 1; 86 }) 87 // 问题1:@Link装饰的变量需要和数据源@State类型一致 88 LinkChild({ testNum: this.testNum.c }) 89 90 // 问题2:@Prop本地没有初始化,也没有从父组件初始化 91 PropChild2() 92 93 // 问题3:PropChild3没有改变@Prop testNum: ClassA的值,所以这时最优的选择是使用@ObjectLink 94 PropChild3({ testNum: this.testNum[0] }) 95 96 ObjectLinkChild({ testNum: this.testNum[0] }) 97 } 98 } 99} 100``` 101 102 103上面的例子有以下几个错误: 104 105 1061. \@Component LinkChild:\@Link testNum: number从父组件的LinkChild({testNum:this.testNum.c})。\@Link的数据源必须是装饰器装饰的状态变量,简而言之,\@Link装饰的数据必须和数据源类型相同,比如\@Link: T和\@State : T。所以,这里应该改为\@Link testNum: ClassA,从父组件初始化的方式为LinkChild({testNum: $testNum})。 107 1082. \@Component PropChild2:\@Prop可以本地初始化,也可以从父组件初始化,但是必须初始化,对于\@Prop testNum: ClassA没有本地初始化,所以必须从父组件初始化PropChild1({testNum: this.testNum})。 109 1103. \@Component PropChild3:没有改变\@Prop testNum: ClassA的值,所以这时较优的选择是使用\@ObjectLink,因为\@Prop是会深拷贝数据,具有拷贝的性能开销,所以这个时候\@ObjectLink是比\@Link和\@Prop更优的选择。 111 1124. 点击ObjectLinkChild给\@ObjectLink装饰的变量赋值:this.testNum = new ClassA(47); 也是不允许的,对于实现双向数据同步的\@ObjectLink,赋值相当于要更新父组件中的数组项或者class的属性,这个对于 TypeScript/JavaScript是不能实现的。框架对于这种行为会发生运行时报错。 113 1145. 如果是非嵌套场景,比如Parent里声明的变量为 \@State testNum: ClassA = new ClassA(1),ClassA就不需要被\@Observed修饰,因为\@State已经具备了观察第一层变化的能力,不需要再使用\@Observed来加一层代理。 115 116 117### 推荐用法 118 119 120 121```ts 122@Observed 123class ClassA { 124 public c: number = 0; 125 126 constructor(c: number) { 127 this.c = c; 128 } 129} 130 131@Component 132struct LinkChild { 133 @Link testNum: ClassA; 134 135 build() { 136 Text(`LinkChild testNum ${this.testNum?.c}`) 137 } 138} 139 140@Component 141struct PropChild1 { 142 @Prop testNum: ClassA = new ClassA(1); 143 144 build() { 145 Text(`PropChild1 testNum ${this.testNum?.c}`) 146 .onClick(() => { 147 this.testNum = new ClassA(48); 148 }) 149 } 150} 151 152@Component 153struct ObjectLinkChild { 154 @ObjectLink testNum: ClassA; 155 156 build() { 157 Text(`ObjectLinkChild testNum ${this.testNum.c}`) 158 // @ObjectLink装饰的变量可以更新属性 159 .onClick(() => { 160 this.testNum.c += 1; 161 }) 162 } 163} 164 165@Entry 166@Component 167struct Parent { 168 @State testNum: ClassA[] = [new ClassA(1)]; 169 170 build() { 171 Column() { 172 Text(`Parent testNum ${this.testNum.c}`) 173 .onClick(() => { 174 this.testNum[0].c += 1; 175 }) 176 // @Link装饰的变量需要和数据源@State类型一致 177 LinkChild({ testNum: this.testNum[0] }) 178 179 // @Prop本地有初始化,不需要再从父组件初始化 180 PropChild1() 181 182 // 当子组件不需要发生本地改变时,优先使用@ObjectLink,因为@Prop是会深拷贝数据,具有拷贝的性能开销,所以这个时候@ObjectLink是比@Link和@Prop更优的选择 183 ObjectLinkChild({ testNum: this.testNum[0] }) 184 } 185 } 186} 187``` 188 189 190 191## 基础嵌套对象属性更改失效 192 193在应用开发中,有很多嵌套对象场景,例如,开发者更新了某个属性,但UI没有进行对应的更新。 194 195每个装饰器都有自己可以观察的能力,并不是所有的改变都可以被观察到,只有可以被观察到的变化才会进行UI更新。\@Observed装饰器可以观察到嵌套对象的属性变化,其他装饰器仅能观察到第二层的变化。 196 197 198### 不推荐用法 199 200下面的例子中,一些UI组件并不会更新。 201 202 203```ts 204class ClassA { 205 a: number; 206 207 constructor(a: number) { 208 this.a = a; 209 } 210 211 getA(): number { 212 return this.a; 213 } 214 215 setA(a: number): void { 216 this.a = a; 217 } 218} 219 220class ClassC { 221 c: number; 222 223 constructor(c: number) { 224 this.c = c; 225 } 226 227 getC(): number { 228 return this.c; 229 } 230 231 setC(c: number): void { 232 this.c = c; 233 } 234} 235 236class ClassB extends ClassA { 237 b: number = 47; 238 c: ClassC; 239 240 constructor(a: number, b: number, c: number) { 241 super(a); 242 this.b = b; 243 this.c = new ClassC(c); 244 } 245 246 getB(): number { 247 return this.b; 248 } 249 250 setB(b: number): void { 251 this.b = b; 252 } 253 254 getC(): number { 255 return this.c.getC(); 256 } 257 258 setC(c: number): void { 259 return this.c.setC(c); 260 } 261} 262 263 264@Entry 265@Component 266struct MyView { 267 @State b: ClassB = new ClassB(10, 20, 30); 268 269 build() { 270 Column({ space: 10 }) { 271 Text(`a: ${this.b.a}`) 272 Button("Change ClassA.a") 273 .onClick(() => { 274 this.b.a += 1; 275 }) 276 277 Text(`b: ${this.b.b}`) 278 Button("Change ClassB.b") 279 .onClick(() => { 280 this.b.b += 1; 281 }) 282 283 Text(`c: ${this.b.c.c}`) 284 Button("Change ClassB.ClassC.c") 285 .onClick(() => { 286 // 点击时上面的Text组件不会刷新 287 this.b.c.c += 1; 288 }) 289 } 290 } 291} 292``` 293 294- 最后一个Text组件Text('c: ${this.b.c.c}'),当点击该组件时UI不会刷新。 因为,\@State b : ClassB 只能观察到this.b属性的变化,比如this.b.a, this.b.b 和this.b.c的变化,但是无法观察嵌套在属性中的属性,即this.b.c.c(属性c是内嵌在b中的对象classC的属性)。 295 296- 为了观察到嵌套与内部的ClassC的属性,需要做如下改变: 297 - 构造一个子组件,用于单独渲染ClassC的实例。 该子组件可以使用\@ObjectLink c : ClassC或\@Prop c : ClassC。通常会使用\@ObjectLink,除非子组件需要对其ClassC对象进行本地修改。 298 - 嵌套的ClassC必须用\@Observed修饰。当在ClassB中创建ClassC对象时(本示例中的ClassB(10, 20, 30)),它将被包装在ES6代理中,当ClassC属性更改时(this.b.c.c += 1),该代码将修改通知到\@ObjectLink变量。 299 300 301### 推荐用法 302 303以下示例使用\@Observed/\@ObjectLink来观察嵌套对象的属性更改。 304 305 306```ts 307class ClassA { 308 a: number; 309 constructor(a: number) { 310 this.a = a; 311 } 312 getA() : number { 313 return this.a; } 314 setA( a: number ) : void { 315 this.a = a; } 316} 317 318@Observed 319class ClassC { 320 c: number; 321 constructor(c: number) { 322 this.c = c; 323 } 324 getC() : number { 325 return this.c; } 326 setC(c : number) : void { 327 this.c = c; } 328} 329 330class ClassB extends ClassA { 331 b: number = 47; 332 c: ClassC; 333 334 constructor(a: number, b: number, c: number) { 335 super(a); 336 this.b = b; 337 this.c = new ClassC(c); 338 } 339 340 getB() : number { 341 return this.b; } 342 setB(b : number) : void { 343 this.b = b; } 344 getC() : number { 345 return this.c.getC(); } 346 setC(c : number) : void { 347 return this.c.setC(c); } 348} 349 350@Component 351struct ViewClassC { 352 353 @ObjectLink c : ClassC; 354 build() { 355 Column({space:10}) { 356 Text(`c: ${this.c.getC()}`) 357 Button("Change C") 358 .onClick(() => { 359 this.c.setC(this.c.getC()+1); 360 }) 361 } 362 } 363} 364 365@Entry 366@Component 367struct MyView { 368 @State b : ClassB = new ClassB(10, 20, 30); 369 370 build() { 371 Column({space:10}) { 372 Text(`a: ${this.b.a}`) 373 Button("Change ClassA.a") 374 .onClick(() => { 375 this.b.a +=1; 376 }) 377 378 Text(`b: ${this.b.b}`) 379 Button("Change ClassB.b") 380 .onClick(() => { 381 this.b.b += 1; 382 }) 383 384 ViewClassC({c: this.b.c}) // Text(`c: ${this.b.c.c}`)的替代写法 385 Button("Change ClassB.ClassC.c") 386 .onClick(() => { 387 this.b.c.c += 1; 388 }) 389 } 390 } 391} 392``` 393 394 395 396## 复杂嵌套对象属性更改失效 397 398 399### 不推荐用法 400 401以下示例创建了一个带有\@ObjectLink装饰变量的子组件,用于渲染一个含有嵌套属性的ParentCounter,用\@Observed装饰嵌套在ParentCounter中的SubCounter。 402 403 404```ts 405let nextId = 1; 406@Observed 407class SubCounter { 408 counter: number; 409 constructor(c: number) { 410 this.counter = c; 411 } 412} 413@Observed 414class ParentCounter { 415 id: number; 416 counter: number; 417 subCounter: SubCounter; 418 incrCounter() { 419 this.counter++; 420 } 421 incrSubCounter(c: number) { 422 this.subCounter.counter += c; 423 } 424 setSubCounter(c: number): void { 425 this.subCounter.counter = c; 426 } 427 constructor(c: number) { 428 this.id = nextId++; 429 this.counter = c; 430 this.subCounter = new SubCounter(c); 431 } 432} 433@Component 434struct CounterComp { 435 @ObjectLink value: ParentCounter; 436 build() { 437 Column({ space: 10 }) { 438 Text(`${this.value.counter}`) 439 .fontSize(25) 440 .onClick(() => { 441 this.value.incrCounter(); 442 }) 443 Text(`${this.value.subCounter.counter}`) 444 .onClick(() => { 445 this.value.incrSubCounter(1); 446 }) 447 Divider().height(2) 448 } 449 } 450} 451@Entry 452@Component 453struct ParentComp { 454 @State counter: ParentCounter[] = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)]; 455 build() { 456 Row() { 457 Column() { 458 CounterComp({ value: this.counter[0] }) 459 CounterComp({ value: this.counter[1] }) 460 CounterComp({ value: this.counter[2] }) 461 Divider().height(5) 462 ForEach(this.counter, 463 (item: ParentCounter) => { 464 CounterComp({ value: item }) 465 }, 466 (item: ParentCounter) => item.id.toString() 467 ) 468 Divider().height(5) 469 // 第一个点击事件 470 Text('Parent: incr counter[0].counter') 471 .fontSize(20).height(50) 472 .onClick(() => { 473 this.counter[0].incrCounter(); 474 // 每次触发时自增10 475 this.counter[0].incrSubCounter(10); 476 }) 477 // 第二个点击事件 478 Text('Parent: set.counter to 10') 479 .fontSize(20).height(50) 480 .onClick(() => { 481 // 无法将value设置为10,UI不会刷新 482 this.counter[0].setSubCounter(10); 483 }) 484 Text('Parent: reset entire counter') 485 .fontSize(20).height(50) 486 .onClick(() => { 487 this.counter = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)]; 488 }) 489 } 490 } 491 } 492} 493``` 494 495对于Text('Parent: incr counter[0].counter')的onClick事件,this.counter[0].incrSubCounter(10)调用incrSubCounter方法使SubCounter的counter值增加10,UI同步刷新。 496 497但是,在Text('Parent: set.counter to 10')的onClick中调用this.counter[0].setSubCounter(10),SubCounter的counter值却无法重置为10。 498 499incrSubCounter和setSubCounter都是同一个SubCounter的函数。在第一个点击处理时调用incrSubCounter可以正确更新UI,而第二个点击处理调用setSubCounter时却没有更新UI。实际上incrSubCounter和setSubCounter两个函数都不能触发Text('${this.value.subCounter.counter}')的更新,因为\@ObjectLink value : ParentCounter仅能观察其代理ParentCounter的属性,对于this.value.subCounter.counter是SubCounter的属性,无法观察到嵌套类的属性。 500 501但是,第一个click事件调用this.counter[0].incrCounter()将CounterComp自定义组件中\@ObjectLink value: ParentCounter标记为已更改。此时触发Text('${this.value.subCounter.counter}')的更新。 如果在第一个点击事件中删除this.counter[0].incrCounter(),也无法更新UI。 502 503 504### 推荐用法 505 506对于上述问题,为了直接观察SubCounter中的属性,以便this.counter[0].setSubCounter(10)操作有效,可以利用下面的方法: 507 508 509```ts 510@ObjectLink value:ParentCounter = new ParentCounter(0); 511@ObjectLink subValue:SubCounter = new SubCounter(0); 512``` 513 514该方法使得\@ObjectLink分别代理了ParentCounter和SubCounter的属性,这样对于这两个类的属性的变化都可以观察到,即都会对UI视图进行刷新。即使删除了上面所说的this.counter[0].incrCounter(),UI也会进行正确的刷新。 515 516该方法可用于实现“两个层级”的观察,即外部对象和内部嵌套对象的观察。但是该方法只能用于\@ObjectLink装饰器,无法作用于\@Prop(\@Prop通过深拷贝传入对象)。详情参考@Prop与@ObjectLink的差异。 517 518 519```ts 520let nextId = 1; 521@Observed 522class SubCounter { 523 counter: number; 524 constructor(c: number) { 525 this.counter = c; 526 } 527} 528@Observed 529class ParentCounter { 530 id: number; 531 counter: number; 532 subCounter: SubCounter; 533 incrCounter() { 534 this.counter++; 535 } 536 incrSubCounter(c: number) { 537 this.subCounter.counter += c; 538 } 539 setSubCounter(c: number): void { 540 this.subCounter.counter = c; 541 } 542 constructor(c: number) { 543 this.id = nextId++; 544 this.counter = c; 545 this.subCounter = new SubCounter(c); 546 } 547} 548@Component 549struct CounterComp { 550 @ObjectLink value: ParentCounter; 551 @ObjectLink subValue: SubCounter; 552 build() { 553 Column({ space: 10 }) { 554 Text(`${this.value.counter}`) 555 .fontSize(25) 556 .onClick(() => { 557 this.value.incrCounter(); 558 }) 559 Text(`${this.subValue.counter}`) 560 .onClick(() => { 561 this.subValue.counter += 1; 562 }) 563 Divider().height(2) 564 } 565 } 566} 567@Entry 568@Component 569struct ParentComp { 570 @State counter: ParentCounter[] = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)]; 571 build() { 572 Row() { 573 Column() { 574 CounterComp({ value: this.counter[0], subValue: this.counter[0].subCounter }) 575 CounterComp({ value: this.counter[1], subValue: this.counter[1].subCounter }) 576 CounterComp({ value: this.counter[2], subValue: this.counter[2].subCounter }) 577 Divider().height(5) 578 ForEach(this.counter, 579 (item: ParentCounter) => { 580 CounterComp({ value: item, subValue: item.subCounter }) 581 }, 582 (item: ParentCounter) => item.id.toString() 583 ) 584 Divider().height(5) 585 Text('Parent: reset entire counter') 586 .fontSize(20).height(50) 587 .onClick(() => { 588 this.counter = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)]; 589 }) 590 Text('Parent: incr counter[0].counter') 591 .fontSize(20).height(50) 592 .onClick(() => { 593 this.counter[0].incrCounter(); 594 this.counter[0].incrSubCounter(10); 595 }) 596 Text('Parent: set.counter to 10') 597 .fontSize(20).height(50) 598 .onClick(() => { 599 this.counter[0].setSubCounter(10); 600 }) 601 } 602 } 603 } 604} 605``` 606 607 608## \@Prop与\@ObjectLink的差异 609 610在下面的示例代码中,\@ObjectLink修饰的变量是对数据源的引用,即在this.value.subValue和this.subValue都是同一个对象的不同引用,所以在点击CounterComp的click handler,改变this.value.subCounter.counter,this.subValue.counter也会改变,对应的组件Text(`this.subValue.counter: ${this.subValue.counter}`)会刷新。 611 612 613```ts 614let nextId = 1; 615 616@Observed 617class SubCounter { 618 counter: number; 619 constructor(c: number) { 620 this.counter = c; 621 } 622} 623 624@Observed 625class ParentCounter { 626 id: number; 627 counter: number; 628 subCounter: SubCounter; 629 incrCounter() { 630 this.counter++; 631 } 632 incrSubCounter(c: number) { 633 this.subCounter.counter += c; 634 } 635 setSubCounter(c: number): void { 636 this.subCounter.counter = c; 637 } 638 constructor(c: number) { 639 this.id = nextId++; 640 this.counter = c; 641 this.subCounter = new SubCounter(c); 642 } 643} 644 645@Component 646struct CounterComp { 647 @ObjectLink value: ParentCounter; 648 @ObjectLink subValue: SubCounter; 649 build() { 650 Column({ space: 10 }) { 651 Text(`this.subValue.counter: ${this.subValue.counter}`) 652 .fontSize(30) 653 Text(`this.value.counter:increase 7 `) 654 .fontSize(30) 655 .onClick(() => { 656 // click handler, Text(`this.subValue.counter: ${this.subValue.counter}`) will update 657 this.value.incrSubCounter(7); 658 }) 659 Divider().height(2) 660 } 661 } 662} 663 664@Entry 665@Component 666struct ParentComp { 667 @State counter: ParentCounter[] = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)]; 668 build() { 669 Row() { 670 Column() { 671 CounterComp({ value: this.counter[0], subValue: this.counter[0].subCounter }) 672 CounterComp({ value: this.counter[1], subValue: this.counter[1].subCounter }) 673 CounterComp({ value: this.counter[2], subValue: this.counter[2].subCounter }) 674 Divider().height(5) 675 ForEach(this.counter, 676 (item: ParentCounter) => { 677 CounterComp({ value: item, subValue: item.subCounter }) 678 }, 679 (item: ParentCounter) => item.id.toString() 680 ) 681 Divider().height(5) 682 Text('Parent: reset entire counter') 683 .fontSize(20).height(50) 684 .onClick(() => { 685 this.counter = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)]; 686 }) 687 Text('Parent: incr counter[0].counter') 688 .fontSize(20).height(50) 689 .onClick(() => { 690 this.counter[0].incrCounter(); 691 this.counter[0].incrSubCounter(10); 692 }) 693 Text('Parent: set.counter to 10') 694 .fontSize(20).height(50) 695 .onClick(() => { 696 this.counter[0].setSubCounter(10); 697 }) 698 } 699 } 700 } 701} 702``` 703 704\@ObjectLink图示如下: 705 706 707 708 709### 不推荐用法 710 711如果用\@Prop替代\@ObjectLink。点击第一个click handler,UI刷新正常。但是点击第二个onClick事件,\@Prop 对变量做了一个本地拷贝,CounterComp的第一个Text并不会刷新。 712 713 this.value.subCounter和this.subValue并不是同一个对象。所以this.value.subCounter的改变,并没有改变this.subValue的拷贝对象,Text(`this.subValue.counter: ${this.subValue.counter}`)不会刷新。 714 715```ts 716@Component 717struct CounterComp { 718 @Prop value: ParentCounter = new ParentCounter(0); 719 @Prop subValue: SubCounter = new SubCounter(0); 720 build() { 721 Column({ space: 10 }) { 722 Text(`this.subValue.counter: ${this.subValue.counter}`) 723 .fontSize(20) 724 .onClick(() => { 725 // 1st click handler 726 this.subValue.counter += 7; 727 }) 728 Text(`this.value.counter:increase 7 `) 729 .fontSize(20) 730 .onClick(() => { 731 // 2nd click handler 732 this.value.incrSubCounter(7); 733 }) 734 Divider().height(2) 735 } 736 } 737} 738``` 739 740\@Prop拷贝的关系图示如下: 741 742 743 744 745### 推荐用法 746 747可以通过从ParentComp到CounterComp仅拷贝一份\@Prop value: ParentCounter,同时必须避免再多拷贝一份SubCounter。 748 749- 在CounterComp组件中只使用一个\@Prop counter:Counter。 750 751- 添加另一个子组件SubCounterComp,其中包含\@ObjectLink subCounter: SubCounter。此\@ObjectLink可确保观察到SubCounter对象属性更改,并且UI更新正常。 752 753- \@ObjectLink subCounter: SubCounter与CounterComp中的\@Prop counter:Counter的this.counter.subCounter共享相同的SubCounter对象。 754 755 756```ts 757let nextId = 1; 758 759@Observed 760class SubCounter { 761 counter: number; 762 constructor(c: number) { 763 this.counter = c; 764 } 765} 766 767@Observed 768class ParentCounter { 769 id: number; 770 counter: number; 771 subCounter: SubCounter; 772 incrCounter() { 773 this.counter++; 774 } 775 incrSubCounter(c: number) { 776 this.subCounter.counter += c; 777 } 778 setSubCounter(c: number): void { 779 this.subCounter.counter = c; 780 } 781 constructor(c: number) { 782 this.id = nextId++; 783 this.counter = c; 784 this.subCounter = new SubCounter(c); 785 } 786} 787 788@Component 789struct SubCounterComp { 790 @ObjectLink subValue: SubCounter; 791 build() { 792 Text(`SubCounterComp: this.subValue.counter: ${this.subValue.counter}`) 793 .onClick(() => { 794 // 2nd click handler 795 this.subValue.counter = 7; 796 }) 797 } 798} 799@Component 800struct CounterComp { 801 @ObjectLink value: ParentCounter; 802 build() { 803 Column({ space: 10 }) { 804 Text(`this.value.incrCounter(): this.value.counter: ${this.value.counter}`) 805 .fontSize(20) 806 .onClick(() => { 807 // 1st click handler 808 this.value.incrCounter(); 809 }) 810 SubCounterComp({ subValue: this.value.subCounter }) 811 Text(`this.value.incrSubCounter()`) 812 .onClick(() => { 813 // 3rd click handler 814 this.value.incrSubCounter(77); 815 }) 816 Divider().height(2) 817 } 818 } 819} 820@Entry 821@Component 822struct ParentComp { 823 @State counter: ParentCounter[] = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)]; 824 build() { 825 Row() { 826 Column() { 827 CounterComp({ value: this.counter[0] }) 828 CounterComp({ value: this.counter[1] }) 829 CounterComp({ value: this.counter[2] }) 830 Divider().height(5) 831 ForEach(this.counter, 832 (item: ParentCounter) => { 833 CounterComp({ value: item }) 834 }, 835 (item: ParentCounter) => item.id.toString() 836 ) 837 Divider().height(5) 838 Text('Parent: reset entire counter') 839 .fontSize(20).height(50) 840 .onClick(() => { 841 this.counter = [new ParentCounter(1), new ParentCounter(2), new ParentCounter(3)]; 842 }) 843 Text('Parent: incr counter[0].counter') 844 .fontSize(20).height(50) 845 .onClick(() => { 846 this.counter[0].incrCounter(); 847 this.counter[0].incrSubCounter(10); 848 }) 849 Text('Parent: set.counter to 10') 850 .fontSize(20).height(50) 851 .onClick(() => { 852 this.counter[0].setSubCounter(10); 853 }) 854 } 855 } 856 } 857} 858``` 859 860 861拷贝关系图示如下: 862 863 864 865 866 867## 应用在渲染期间禁止改变状态变量 868 869在学习本示例之前,我们要先明确一个概念,在ArkUI状态管理中,状态驱动UI更新。 870 871 872 873所以,不能在自定义组件的build()或\@Builder方法里直接改变状态变量,这可能会造成循环渲染的风险,下面以build()方法举例示意。 874 875 876### 不推荐用法 877 878在下面的示例中,Text('${this.count++}')在build渲染方法里直接改变了状态变量。 879 880 881```ts 882@Entry 883@Component 884struct CompA { 885 @State col1: Color = Color.Yellow; 886 @State col2: Color = Color.Green; 887 @State count: number = 1; 888 build() { 889 Column() { 890 // 应避免直接在Text组件内改变count的值 891 Text(`${this.count++}`) 892 .width(50) 893 .height(50) 894 .fontColor(this.col1) 895 .onClick(() => { 896 this.col2 = Color.Red; 897 }) 898 Button("change col1").onClick(() =>{ 899 this.col1 = Color.Pink; 900 }) 901 } 902 .backgroundColor(this.col2) 903 } 904} 905``` 906 907在ArkUI中,Text('${this.count++}')在全量更新或最小化更新会产生不同的影响: 908 909- 全量更新: ArkUI可能会陷入一个无限的重渲染的循环里,因为Text组件的每一次渲染都会改变应用的状态,就会再引起下一轮渲染的开启。 当 this.col2 更改时,都会执行整个build构建函数,因此,Text(`${this.count++}`)绑定的文本也会更改,每次重新渲染Text(`${this.count++}`),又会使this.count状态变量更新,导致新一轮的build执行,从而陷入无限循环。 910 911- 最小化更新: 当 this.col2 更改时,只有Column组件会更新,Text组件不会更改。 只当 this.col1 更改时,会去更新整个Text组件,其所有属性函数都会执行,所以会看到Text(`${this.count++}`)自增。因为目前UI以组件为单位进行更新,如果组件上某一个属性发生改变,会更新整体的组件。所以整体的更新链路是:this.col2 = Color.Red -> Text组件整体更新->this.count++->Text组件整体更新。 912 913 914### 推荐用法 915 916建议应用的开发方法在事件处理程序中执行count++操作。 917 918 919```ts 920@Entry 921@Component 922struct CompA { 923 @State col1: Color = Color.Yellow; 924 @State col2: Color = Color.Green; 925 @State count: number = 1; 926 build() { 927 Column() { 928 Text(`${this.count}`) 929 .width(50) 930 .height(50) 931 .backgroundColor(this.col1) 932 .onClick(() => { 933 this.count++; 934 }) 935 } 936 .backgroundColor(this.col2) 937 } 938} 939``` 940 941build函数中更改应用状态的行为可能会比上面的示例更加隐蔽,比如: 942 943- 在\@Builder,\@Extend或\@Styles方法内改变状态变量 。 944 945- 在计算参数时调用函数中改变应用状态变量,例如 Text('${this.calcLabel()}')。 946 947- 对当前数组做出修改,sort()改变了数组this.arr,随后的filter方法会返回一个新的数组。 948 949 950```ts 951@State arr : Array<...> = [ ... ]; 952ForEach(this.arr.sort().filter(...), 953 item => { 954 ... 955}) 956``` 957 958正确的执行方式为:filter返回一个新数组,后面的sort方法才不会改变原数组this.arr,示例: 959 960 961```ts 962ForEach(this.arr.filter(...).sort(), 963 item => { 964 ... 965}) 966``` 967 968 969## 使用状态变量强行更新 970 971 972### 不推荐用法 973 974 975```ts 976@Entry 977@Component 978struct CompA { 979 @State needsUpdate: boolean = true; 980 realState1: Array<number> = [4, 1, 3, 2]; // 未使用状态变量装饰器 981 realState2: Color = Color.Yellow; 982 983 updateUI1(param: Array<number>): Array<number> { 984 const triggerAGet = this.needsUpdate; 985 return param; 986 } 987 updateUI2(param: Color): Color { 988 const triggerAGet = this.needsUpdate; 989 return param; 990 } 991 build() { 992 Column({ space: 20 }) { 993 ForEach(this.updateUI1(this.realState1), 994 (item: Array<number>) => { 995 Text(`${item}`) 996 }) 997 Text("add item") 998 .onClick(() => { 999 // 改变realState1不会触发UI视图更新 1000 this.realState1.push(this.realState1[this.realState1.length-1] + 1); 1001 1002 // 触发UI视图更新 1003 this.needsUpdate = !this.needsUpdate; 1004 }) 1005 Text("chg color") 1006 .onClick(() => { 1007 // 改变realState2不会触发UI视图更新 1008 this.realState2 = this.realState2 == Color.Yellow ? Color.Red : Color.Yellow; 1009 1010 // 触发UI视图更新 1011 this.needsUpdate = !this.needsUpdate; 1012 }) 1013 }.backgroundColor(this.updateUI2(this.realState2)) 1014 .width(200).height(500) 1015 } 1016} 1017``` 1018 1019上述示例存在以下问题: 1020 1021- 应用程序希望控制UI更新逻辑,但在ArkUI中,UI更新的逻辑应该是由框架来检测应用程序状态变量的更改去实现。 1022 1023- this.needsUpdate是一个自定义的UI状态变量,应该仅应用于其绑定的UI组件。变量this.realState1、this.realState2没有被装饰,他们的变化将不会触发UI刷新。 1024 1025- 但是在该应用中,用户试图通过this.needsUpdate的更新来带动常规变量this.realState1、this.realState2的更新。此方法不合理且更新性能较差,如果只想更新背景颜色,且不需要更新ForEach,但this.needsUpdate值的变化也会带动ForEach更新。 1026 1027 1028### 推荐用法 1029 1030要解决此问题,应将realState1和realState2成员变量用\@State装饰。一旦完成此操作,就不再需要变量needsUpdate。 1031 1032 1033```ts 1034@Entry 1035@Component 1036struct CompA { 1037 @State realState1: Array<number> = [4, 1, 3, 2]; 1038 @State realState2: Color = Color.Yellow; 1039 build() { 1040 Column({ space: 20 }) { 1041 ForEach(this.realState1, 1042 (item: Array<number>) => { 1043 Text(`${item}`) 1044 }) 1045 Text("add item") 1046 .onClick(() => { 1047 // 改变realState1触发UI视图更新 1048 this.realState1.push(this.realState1[this.realState1.length-1] + 1); 1049 }) 1050 Text("chg color") 1051 .onClick(() => { 1052 // 改变realState2触发UI视图更新 1053 this.realState2 = this.realState2 == Color.Yellow ? Color.Red : Color.Yellow; 1054 }) 1055 }.backgroundColor(this.realState2) 1056 .width(200).height(500) 1057 } 1058} 1059``` 1060 1061## 精准控制状态变量关联的组件数 1062 1063精准控制状态变量关联的组件数能减少不必要的组件刷新,提高组件的刷新效率。有时开发者会将同一个状态变量绑定多个同级组件的属性,当状态变量改变时,会让这些组件做出相同的改变,这有时会造成组件的不必要刷新,如果存在某些比较复杂的组件,则会大大影响整体的性能。但是如果将这个状态变量绑定在这些同级组件的父组件上,则可以减少需要刷新的组件数,从而提高刷新的性能。 1064 1065### 不推荐用法 1066 1067```ts 1068@Observed 1069class Translate { 1070 translateX: number = 20; 1071} 1072@Component 1073struct Title { 1074 @ObjectLink translateObj: Translate; 1075 build() { 1076 Row() { 1077 Image($r('app.media.icon')) 1078 .width(50) 1079 .height(50) 1080 .translate({ 1081 x:this.translateObj.translateX // this.translateObj.translateX used in two component both in Row 1082 }) 1083 Text("Title") 1084 .fontSize(20) 1085 .translate({ 1086 x: this.translateObj.translateX 1087 }) 1088 } 1089 } 1090} 1091@Entry 1092@Component 1093struct Page { 1094 @State translateObj: Translate = new Translate(); 1095 build() { 1096 Column() { 1097 Title({ 1098 translateObj: this.translateObj 1099 }) 1100 Stack() { 1101 } 1102 .backgroundColor("black") 1103 .width(200) 1104 .height(400) 1105 .translate({ 1106 x:this.translateObj.translateX //this.translateObj.translateX used in two components both in Column 1107 }) 1108 Button("move") 1109 .translate({ 1110 x:this.translateObj.translateX 1111 }) 1112 .onClick(() => { 1113 animateTo({ 1114 duration: 50 1115 },()=>{ 1116 this.translateObj.translateX = (this.translateObj.translateX + 50) % 150 1117 }) 1118 }) 1119 } 1120 } 1121} 1122``` 1123 1124在上面的示例中,状态变量this.translateObj.translateX被用在多个同级的子组件下,当this.translateObj.translateX变化时,会导致所有关联它的组件一起刷新,但实际上由于这些组件的变化是相同的,因此可以将这个属性绑定到他们共同的父组件上,来实现减少组件的刷新数量。经过分析,所有的子组件其实都处于Page下的Column中,因此将所有子组件相同的translate属性统一到Column上,来实现精准控制状态变量关联的组件数。 1125 1126### 推荐用法 1127 1128``` 1129@Observed 1130class Translate { 1131 translateX: number = 20; 1132} 1133@Component 1134struct Title { 1135 @ObjectLink translateObj: Translate; 1136 build() { 1137 Row() { 1138 Image($r('app.media.icon')) 1139 .width(50) 1140 .height(50) 1141 Text("Title") 1142 .fontSize(20) 1143 } 1144 } 1145} 1146@Entry 1147@Component 1148struct Page { 1149 @State translateObj: Translate = new Translate(); 1150 build() { 1151 Column() { 1152 Title({ 1153 translateObj: this.translateObj 1154 }) 1155 Stack() { 1156 } 1157 .backgroundColor("black") 1158 .width(200) 1159 .height(400) 1160 Button("move") 1161 .onClick(() => { 1162 animateTo({ 1163 duration: 50 1164 },()=>{ 1165 this.translateObj.translateX = (this.translateObj.translateX + 50) % 150 1166 }) 1167 }) 1168 } 1169 .translate({ // the component in Column shares the same property translate 1170 x: this.translateObj.translateX 1171 }) 1172 } 1173} 1174```