1# \@Reusable装饰器:组件复用 2 3 4\@Reusable装饰器装饰任意自定义组件时,表示该自定义组件可以复用。 5 6> **说明:** 7> 8> 从API version 10开始,对\@Reusable进行支持,支持在ArkTS中使用。 9 10 11 12## 概述 13 14- \@Reusable适用自定义组件,与\@Component结合使用,标记为\@Reusable的自定义组件从组件树上被移除时,组件和其对应的JSView对象都会被放入复用缓存中,后续创建新自定义组件节点时,会复用缓存区中的节点,节约组件重新创建的时间。 15 16## 限制条件 17 18- \@Reusable装饰器仅用于自定义组件。 19 20```ts 21// 编译报错,仅用于自定义组件 22 @Reusable 23 @Builder 24 function buildCreativeLoadingDialog(closedClick: () => void) { 25 Crash() 26 } 27 28``` 29 30- ComponentContent不支持传入\@Reusable装饰器装饰的自定义组件。 31 32```ts 33@Builder 34function buildCreativeLoadingDialog(closedClick: () => void) { 35 Crash() 36} 37 38// 如果注释掉就可以正常弹出弹窗,如果加上@Reusable就直接crash 39@Reusable 40@Component 41export struct Crash { 42 build() { 43 Column() { 44 Text("Crash") 45 .fontSize(12) 46 .lineHeight(18) 47 .fontColor(Color.Blue) 48 .margin({ 49 left: 6 50 }) 51 }.width('100%') 52 .height('100%') 53 .justifyContent(FlexAlign.Center) 54 } 55} 56 57@Entry 58@Component 59struct Index { 60 @State message: string = 'Hello World'; 61 private uicontext = this.getUIContext() 62 63 build() { 64 RelativeContainer() { 65 Text(this.message) 66 .id('Index') 67 .fontSize(50) 68 .fontWeight(FontWeight.Bold) 69 .alignRules({ 70 center: { anchor: '__container__', align: VerticalAlign.Center }, 71 middle: { anchor: '__container__', align: HorizontalAlign.Center } 72 }) 73 .onClick(() => { 74 // ComponentContent 底层时buildNode,buildNode不支持传入@Reusable注解的自定义组件 75 let contentNode = new ComponentContent(this.uicontext, wrapBuilder(buildCreativeLoadingDialog), () => { 76 }); 77 this.uicontext.getPromptAction().openCustomDialog(contentNode); 78 }) 79 } 80 .height('100%') 81 .width('100%') 82 } 83} 84``` 85 86- \@Reusable装饰器不支持嵌套使用。 87 88```ts 89// Parent 被标记@Reusable 90@Reusable 91@Component 92export struct Parent{ 93 build() { 94 Column() { 95 // 问题用法,编译不报错,有时显示正常,可能导致未定义的结果,不建议使用此用法 96 // 可复用的组件的子树中存在可复用的组件,可能导致未定义的结果 97 HasReusableChild() 98 Text("Parent") 99 .fontSize(12) 100 .lineHeight(18) 101 .fontColor(Color.Blue) 102 .margin({ 103 left: 6 104 }) 105 }.width('100%') 106 .height('100%') 107 .justifyContent(FlexAlign.Center) 108 } 109} 110 111// 子自定义组件被也被标记@Reusable 112@Reusable 113@Component 114export struct HasReusableChild { 115 build() { 116 Column() { 117 Text("hasReusableChild") 118 .fontSize(12) 119 .lineHeight(18) 120 .fontColor(Color.Blue) 121 .margin({ 122 left: 6 123 }) 124 }.width('100%') 125 .height('100%') 126 .justifyContent(FlexAlign.Center) 127 } 128} 129``` 130 131## 使用场景 132 133- 列表滚动:当应用需要展示大量数据的列表,并且用户进行滚动操作时,频繁创建和销毁列表项的视图可能导致卡顿和性能问题。在这种情况下,使用列表组件的组件复用机制可以重用已经创建的列表项视图,提高滚动的流畅度。 134 135- 动态布局更新:如果应用中的界面需要频繁地进行布局更新,例如根据用户的操作或数据变化动态改变视图结构和样式,重复创建和销毁视图可能导致频繁的布局计算,影响帧率。在这种情况下,使用组件复用可以避免不必要的视图创建和布局计算,提高性能。 136 137- 频繁创建和销毁数据项的视图场景下。使用组件复用可以重用已创建的视图,只更新数据的内容,减少视图的创建和销毁,能有效提高性能。 138 139 140## 使用场景举例 141 142### 动态布局更新 143 144- 示例代码将Child自定义组件标记为复用组件,通过Button点击更新Child,触发Child复用; 145- \@Reusable:自定义组件被\@Reusable装饰器修饰,即表示其具备组件复用的能力; 146- aboutToReuse:当一个可复用的自定义组件从复用缓存中重新加入到节点树时,触发aboutToReuse生命周期回调,并将组件的构造参数传递给aboutToReuse; 147 148```ts 149// xxx.ets 150export class Message { 151 value: string | undefined; 152 153 constructor(value: string) { 154 this.value = value; 155 } 156} 157 158@Entry 159@Component 160struct Index { 161 @State switch: boolean = true; 162 build() { 163 Column() { 164 Button('Hello') 165 .fontSize(30) 166 .fontWeight(FontWeight.Bold) 167 .onClick(() => { 168 this.switch = !this.switch; 169 }) 170 if (this.switch) { 171 Child({ message: new Message('Child') }) 172 // 如果只有一个复用的组件,可以不用设置reuseId 173 .reuseId('Child') 174 } 175 } 176 .height("100%") 177 .width('100%') 178 } 179} 180 181@Reusable 182@Component 183struct Child { 184 @State message: Message = new Message('AboutToReuse'); 185 186 aboutToReuse(params: Record<string, ESObject>) { 187 console.info("Recycle ====Child=="); 188 this.message = params.message as Message; 189 } 190 191 build() { 192 Column() { 193 Text(this.message.value) 194 .fontSize(30) 195 } 196 .borderWidth(1) 197 .height(100) 198 } 199} 200``` 201 202### 列表滚动配合LazyForEach使用 203 204- 示例代码将CardView自定义组件标记为复用组件,List上下滑动,触发CardView复用; 205- \@Reusable:自定义组件被@Reusable装饰器修饰,即表示其具备组件复用的能力; 206- 变量item的被\@State修饰,才能更新,非\@State修饰变量存在无法更新问题; 207 208```ts 209class MyDataSource implements IDataSource { 210 private dataArray: string[] = []; 211 private listener: DataChangeListener | undefined; 212 213 public totalCount(): number { 214 return this.dataArray.length; 215 } 216 217 public getData(index: number): string { 218 return this.dataArray[index]; 219 } 220 221 public pushData(data: string): void { 222 this.dataArray.push(data); 223 } 224 225 public reloadListener(): void { 226 this.listener?.onDataReloaded(); 227 } 228 229 public registerDataChangeListener(listener: DataChangeListener): void { 230 this.listener = listener; 231 } 232 233 public unregisterDataChangeListener(listener: DataChangeListener): void { 234 this.listener = undefined; 235 } 236} 237 238@Entry 239@Component 240struct ReuseDemo { 241 private data: MyDataSource = new MyDataSource(); 242 243 // ... 244 build() { 245 Column() { 246 List() { 247 LazyForEach(this.data, (item: string) => { 248 ListItem() { 249 CardView({ item: item }) 250 } 251 }, (item: string) => item) 252 } 253 } 254 } 255} 256 257// 复用组件 258@Reusable 259@Component 260export struct CardView { 261 @State item: string = ''; 262 263 aboutToReuse(params: Record<string, Object>): void { 264 this.item = params.item as string; 265 } 266 267 build() { 268 Column() { 269 Text(this.item) 270 .fontSize(30) 271 } 272 .borderWidth(1) 273 .height(100) 274 } 275} 276``` 277 278### if使用场景 279 280- 示例代码将OneMoment自定义组件标记为复用组件,List上下滑动,触发OneMoment复用; 281- 可以使用reuseId为复用组件分配复用组,相同reuseId的组件会在同一个复用组中复用,如果只有一个复用的组件,可以不用设置reuseId; 282- 通过reuseId来标识需要复用的组件,省去重复执行if的删除重创逻辑,提高组件复用的效率和性能; 283 284```ts 285@Entry 286@Component 287struct withoutReuseId { 288 aboutToAppear(): void { 289 getFriendMomentFromRawfile(); 290 } 291 292 build() { 293 Column() { 294 TopBar() 295 List({ space: ListConstants.LIST_SPACE }) { 296 LazyForEach(momentData, (moment: FriendMoment) => { 297 ListItem() { 298 OneMoment({moment: moment}) 299 // 使用reuseId进行组件复用的控制 300 .reuseId((moment.image !== '') ? 'withImage' : 'noImage') 301 } 302 }, (moment: FriendMoment) => moment.id) 303 } 304 .cachedCount(Constants.CACHED_COUNT) 305 } 306 } 307} 308 309@Reusable 310@Component 311export struct OneMoment { 312 @Prop moment: FriendMoment; 313 314 build() { 315 Column() { 316 ... 317 Text(this.moment.text) 318 319 if (this.moment.image !== '') { 320 Flex({ wrap: FlexWrap.Wrap }) { 321 Image($r(this.moment.image)) 322 Image($r(this.moment.image)) 323 Image($r(this.moment.image)) 324 Image($r(this.moment.image)) 325 } 326 } 327 ... 328 } 329 } 330} 331``` 332 333### foreach使用场景 334 335- 示例点击update,数据刷新成功,但是滑动列表,组件复用无法使用,foreach的折叠展开属性的原因; 336- 点击clear,再次update,复用成功;符合一帧内重复创建多个已被销毁的自定义组件; 337 338```ts 339// xxx.ets 340class MyDataSource implements IDataSource { 341 private dataArray: string[] = []; 342 343 public totalCount(): number { 344 return this.dataArray.length; 345 } 346 347 public getData(index: number): string { 348 return this.dataArray[index]; 349 } 350 351 public pushData(data: string): void { 352 this.dataArray.push(data); 353 } 354 355 public registerDataChangeListener(listener: DataChangeListener): void { 356 } 357 358 public unregisterDataChangeListener(listener: DataChangeListener): void { 359 } 360} 361 362@Entry 363@Component 364struct Index { 365 private data: MyDataSource = new MyDataSource(); 366 private data02: MyDataSource = new MyDataSource(); 367 @State isShow: boolean = true 368 @State dataSource: ListItemObject[] = []; 369 370 aboutToAppear() { 371 for (let i = 0; i < 100; i++) { 372 this.data.pushData(i.toString()) 373 } 374 375 for (let i = 30; i < 80; i++) { 376 this.data02.pushData(i.toString()) 377 } 378 } 379 380 build() { 381 Column() { 382 Row() { 383 Button('clear').onClick(() => { 384 for (let i = 1; i < 50; i++) { 385 let obj = new ListItemObject(); 386 obj.id = i; 387 obj.uuid = Math.random().toString(); 388 obj.isExpand = false 389 this.dataSource.pop(); 390 } 391 }).height(40) 392 393 Button('update').onClick(() => { 394 for (let i = 1; i < 50; i++) { 395 let obj = new ListItemObject(); 396 obj.id = i; 397 obj.uuid = Math.random().toString(); 398 obj.isExpand = false 399 this.dataSource.push(obj); 400 } 401 }).height(40) 402 } 403 404 List({ space: 10 }) { 405 ForEach(this.dataSource, (item: ListItemObject) => { 406 ListItem() { 407 ListItemView({ 408 obj: item 409 }) 410 } 411 }, (item: ListItemObject) => { 412 return item.uuid.toString() 413 }) 414 415 }.cachedCount(0) 416 .width('100%') 417 .height('100%') 418 } 419 } 420} 421 422@Reusable 423@Component 424struct ListItemView { 425 @ObjectLink obj: ListItemObject; 426 @State item: string = '' 427 428 aboutToAppear(): void { 429 // 点击 update,首次进入,上下滑动,由于foreach折叠展开属性,无法复用 430 console.log("=====abouTo===Appear=====ListItemView==创建了==" + this.item) 431 } 432 433 aboutToReuse(params: ESObject) { 434 this.item = params.item; 435 // 点击 clear,再次update ,复用成功 436 //符合一帧内重复创建多个已被销毁的自定义组件 437 console.log("=====aboutTo===Reuse====ListItemView==复用了==" + this.item) 438 } 439 440 build() { 441 Column({ space: 10 }) { 442 Text(`${this.obj.id}.标题`) 443 .fontSize(16) 444 .fontColor('#000000') 445 .padding({ 446 top: 20, 447 bottom: 20, 448 }) 449 450 if (this.obj.isExpand) { 451 Text('') 452 .fontSize(14) 453 .fontColor('#999999') 454 } 455 } 456 .width('100%') 457 .borderRadius(10) 458 .backgroundColor(Color.White) 459 .padding(15) 460 .onClick(() => { 461 this.obj.isExpand = !this.obj.isExpand; 462 }) 463 } 464} 465 466@Observed 467class ListItemObject { 468 uuid: string = "" 469 id: number = 0; 470 isExpand: boolean = false; 471} 472``` 473 474### Grid使用场景 475 476- 示例中使用\@Reusable装饰器修饰GridItem中的自定义组件ReusableChildComponent,即表示其具备组件复用的能力; 477- 使用aboutToReuse是为了让Grid在滑动时从复用缓存中加入到组件树之前触发,用于更新组件的状态变量以展示正确的内容; 478- 需要注意的是无需在aboutToReuse中对\@Link、\@StorageLink、\@ObjectLink、\@Consume等自动更新值的状态变量进行更新,可能触发不必要的组件刷新。 479 480```ts 481// MyDataSource类实现IDataSource接口 482class MyDataSource implements IDataSource { 483 private dataArray: number[] = []; 484 485 public pushData(data: number): void { 486 this.dataArray.push(data); 487 } 488 489 // 数据源的数据总量 490 public totalCount(): number { 491 return this.dataArray.length; 492 } 493 494 // 返回指定索引位置的数据 495 public getData(index: number): number { 496 return this.dataArray[index]; 497 } 498 499 registerDataChangeListener(listener: DataChangeListener): void { 500 } 501 502 unregisterDataChangeListener(listener: DataChangeListener): void { 503 } 504} 505 506@Entry 507@Component 508struct MyComponent { 509 // 数据源 510 private data: MyDataSource = new MyDataSource(); 511 512 aboutToAppear() { 513 for (let i = 1; i < 1000; i++) { 514 this.data.pushData(i); 515 } 516 } 517 518 build() { 519 Column({ space: 5 }) { 520 Grid() { 521 LazyForEach(this.data, (item: number) => { 522 GridItem() { 523 // 使用可复用自定义组件 524 ReusableChildComponent({ item: item }) 525 } 526 }, (item: string) => item) 527 } 528 .cachedCount(2) // 设置GridItem的缓存数量 529 .columnsTemplate('1fr 1fr 1fr') 530 .columnsGap(10) 531 .rowsGap(10) 532 .margin(10) 533 .height(500) 534 .backgroundColor(0xFAEEE0) 535 } 536 } 537} 538 539// 自定义组件被@Reusable装饰器修饰,即标志其具备组件复用的能力 540@Reusable 541@Component 542struct ReusableChildComponent { 543 @State item: number = 0; 544 545 // aboutToReuse从复用缓存中加入到组件树之前调用,可在此处更新组件的状态变量以展示正确的内容 546 // aboutToReuse参数类型已不支持any,这里使用Record指定明确的数据类型。Record用于构造一个对象类型,其属性键为Keys,属性值为Type 547 aboutToReuse(params: Record<string, number>) { 548 this.item = params.item; 549 } 550 551 build() { 552 Column() { 553 Image($r('app.media.icon')) 554 .objectFit(ImageFit.Fill) 555 .layoutWeight(1) 556 Text(`图片${this.item}`) 557 .fontSize(16) 558 .textAlign(TextAlign.Center) 559 } 560 .width('100%') 561 .height(120) 562 .backgroundColor(0xF9CF93) 563 } 564} 565``` 566 567### WaterFlow使用场景 568 569- WaterFlow滑动场景存在FlowItem及其子组件的频繁创建和销毁,可以将FlowItem中的组件封装成自定义组件,并使用\@Reusable装饰器修饰,使其具备组件复用能力; 570 571```ts 572 build() { 573 Column({ space: 2 }) { 574 WaterFlow() { 575 LazyForEach(this.datasource, (item: number) => { 576 FlowItem() { 577 // 使用可复用自定义组件 578 ReusableFlowItem({ item: item }) 579 } 580 .onAppear(() => { 581 // 即将触底时提前增加数据 582 if (item + 20 == this.datasource.totalCount()) { 583 for (let i = 0; i < 100; i++) { 584 this.datasource.AddLastItem() 585 } 586 } 587 }) 588 .width('100%') 589 .height(this.itemHeightArray[item % 100]) 590 .backgroundColor(this.colors[item % 5]) 591 }, (item: string) => item) 592 } 593 .columnsTemplate("1fr 1fr") 594 .columnsGap(10) 595 .rowsGap(5) 596 .backgroundColor(0xFAEEE0) 597 .width('100%') 598 .height('80%') 599 } 600 } 601@Reusable 602@Component 603struct ReusableFlowItem { 604 @State item: number = 0 605 606 // 从复用缓存中加入到组件树之前调用,可在此处更新组件的状态变量以展示正确的内容 607 aboutToReuse(params) { 608 this.item = params.item; 609 } 610 611 build() { 612 Column() { 613 Text("N" + this.item).fontSize(12).height('16') 614 Image('res/waterFlowTest (' + this.item % 5 + ').jpg') 615 .objectFit(ImageFit.Fill) 616 .width('100%') 617 .layoutWeight(1) 618 } 619 } 620} 621``` 622 623### 多种条目类型使用场景 624 625#### 标准型 626 627- 复用组件之间布局完全相同; 628- 示例同列表滚动中描述; 629 630#### 有限变化型 631 632- 复用组件之间有不同,但是类型有限; 633- 示例为复用组件显式设置两个reuseId与使用两个自定义组件进行复用; 634 635```ts 636class MyDataSource implements IDataSource { 637 ... 638} 639 640@Entry 641@Component 642struct Index { 643 private data: MyDataSource = new MyDataSource(); 644 645 aboutToAppear() { 646 for (let i = 0; i < 1000; i++) { 647 this.data.pushData(i); 648 } 649 } 650 651 build() { 652 Column() { 653 List({ space: 10 }) { 654 LazyForEach(this.data, (item: number) => { 655 ListItem() { 656 ReusableComponent({ item: item }) 657 .reuseId(item % 2 === 0 ? 'ReusableComponentOne' : 'ReusableComponentTwo') 658 } 659 .backgroundColor(Color.Orange) 660 .width('100%') 661 }, (item: number) => item.toString()) 662 } 663 .cachedCount(2) 664 } 665 } 666} 667 668@Reusable 669@Component 670struct ReusableComponent { 671 @State item: number = 0; 672 673 aboutToReuse(params: ESObject) { 674 this.item = params.item; 675 } 676 677 build() { 678 Column() { 679 if (this.item % 2 === 0) { 680 Text(`Item ${this.item} ReusableComponentOne`) 681 .fontSize(20) 682 .margin({ left: 10 }) 683 } else { 684 Text(`Item ${this.item} ReusableComponentTwo`) 685 .fontSize(20) 686 .margin({ left: 10 }) 687 } 688 }.margin({ left: 10, right: 10 }) 689 } 690} 691 692``` 693 694#### 组合型 695 696- 复用组件之间有不同,情况非常多,但是拥有共同的子组件; 697- 示例按照组合型的组件复用方式,将上述示例中的三种复用组件转变为Builder函数后,内部共同的子组件就处于同一个父组件MyComponent下; 698- 对这些子组件使用组件复用时,它们的缓存池也会在父组件上共享,节省组件创建时的消耗。 699 700```ts 701class MyDataSource implements IDataSource { 702 ... 703} 704 705@Entry 706@Component 707struct MyComponent { 708 private data: MyDataSource = new MyDataSource(); 709 710 aboutToAppear() { 711 for (let i = 0; i < 1000; i++) { 712 this.data.pushData(i.toString()) 713 } 714 } 715 716 @Builder 717 itemBuilderOne(item: string) { 718 Column() { 719 ChildComponentA({ item: item }) 720 ChildComponentB({ item: item }) 721 ChildComponentC({ item: item }) 722 } 723 } 724 725 @Builder 726 itemBuilderTwo(item: string) { 727 Column() { 728 ChildComponentA({ item: item }) 729 ChildComponentC({ item: item }) 730 ChildComponentD({ item: item }) 731 } 732 } 733 734 @Builder 735 itemBuilderThree(item: string) { 736 Column() { 737 ChildComponentA({ item: item }) 738 ChildComponentB({ item: item }) 739 ChildComponentD({ item: item }) 740 } 741 } 742 743 build() { 744 List({ space: 40 }) { 745 LazyForEach(this.data, (item: string, index: number) => { 746 ListItem() { 747 if (index % 3 === 0) { 748 this.itemBuilderOne(item) 749 } else if (index % 5 === 0) { 750 this.itemBuilderTwo(item) 751 } else { 752 this.itemBuilderThree(item) 753 } 754 } 755 .backgroundColor('#cccccc') 756 .width('100%') 757 .onAppear(() => { 758 console.log(`ListItem ${index} onAppear`); 759 }) 760 }, (item: number) => item.toString()) 761 } 762 .width('100%') 763 .height('100%') 764 .cachedCount(0) 765 } 766} 767 768@Reusable 769@Component 770struct ChildComponentA { 771 @State item: string = ''; 772 773 aboutToReuse(params: ESObject) { 774 console.log(`ChildComponentA ${params.item} Reuse ${this.item}`); 775 this.item = params.item; 776 } 777 778 aboutToRecycle(): void { 779 console.log(`ChildComponentA ${this.item} Recycle`); 780 } 781 782 build() { 783 Column() { 784 Text(`Item ${this.item} Child Component A`) 785 .fontSize(20) 786 .margin({ left: 10 }) 787 .fontColor(Color.Blue) 788 Grid() { 789 ForEach((new Array(20)).fill(''), (item: string,index: number) => { 790 GridItem() { 791 Image($r('app.media.startIcon')) 792 .height(20) 793 } 794 }) 795 } 796 .columnsTemplate('1fr 1fr 1fr 1fr 1fr') 797 .rowsTemplate('1fr 1fr 1fr 1fr') 798 .columnsGap(10) 799 .width('90%') 800 .height(160) 801 } 802 .margin({ left: 10, right: 10 }) 803 .backgroundColor(0xFAEEE0) 804 } 805} 806 807@Reusable 808@Component 809struct ChildComponentB { 810 @State item: string = ''; 811 812 aboutToReuse(params: ESObject) { 813 this.item = params.item; 814 } 815 816 build() { 817 Row() { 818 Text(`Item ${this.item} Child Component B`) 819 .fontSize(20) 820 .margin({ left: 10 }) 821 .fontColor(Color.Red) 822 }.margin({ left: 10, right: 10 }) 823 } 824} 825 826@Reusable 827@Component 828struct ChildComponentC { 829 @State item: string = ''; 830 831 aboutToReuse(params: ESObject) { 832 this.item = params.item; 833 } 834 835 build() { 836 Row() { 837 Text(`Item ${this.item} Child Component C`) 838 .fontSize(20) 839 .margin({ left: 10 }) 840 .fontColor(Color.Green) 841 }.margin({ left: 10, right: 10 }) 842 } 843} 844 845@Reusable 846@Component 847struct ChildComponentD { 848 @State item: string = ''; 849 850 aboutToReuse(params: ESObject) { 851 this.item = params.item; 852 } 853 854 build() { 855 Row() { 856 Text(`Item ${this.item} Child Component D`) 857 .fontSize(20) 858 .margin({ left: 10 }) 859 .fontColor(Color.Orange) 860 }.margin({ left: 10, right: 10 }) 861 } 862} 863``` 864