1# \@Reusable装饰器:组件复用 2<!--Kit: ArkUI--> 3<!--Subsystem: ArkUI--> 4<!--Owner: @liyujie43--> 5<!--Designer: @lizhan--> 6<!--Tester: @TerryTsao--> 7<!--Adviser: @zhang_yixin13--> 8 9\@Reusable装饰器标记的自定义组件支持视图节点、组件实例和状态上下文的复用,避免重复创建和销毁,提升性能。 10 11## 概述 12 13使用\@Reusable装饰器时,表示该自定义组件可以复用。与[\@Component装饰器](arkts-create-custom-components.md#component)结合使用,标记为\@Reusable的自定义组件在从组件树中移除时,组件及其对应的JS对象将被放入复用缓存中。后续创建新自定义组件节点时,将复用缓存中的节点,从而节约组件重新创建的时间。 14 15> **说明:** 16> 17> API version 10开始支持@Reusable,支持在ArkTS中使用。 18> 19> 关于组件复用的原理与使用、优化方法、适用场景,请参考最佳实践[组件复用最佳实践](https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-component-reuse)。 20> 21> \@Reusable标识之后,在组件上下树时ArkUI框架会调用该组件的[aboutToReuse](../../../application-dev/reference/apis-arkui/arkui-ts/ts-custom-component-lifecycle.md#abouttoreuse10)方法和[aboutToRecycle](../../../application-dev/reference/apis-arkui/arkui-ts/ts-custom-component-lifecycle.md#abouttorecycle10)方法,因此,开发者在实现复用时,大部分代码都集中在这两个生命周期方法中。 22> 23> 如果一个组件里可复用的组件不止一个,可以使用[reuseId](../../../application-dev/reference/apis-arkui/arkui-ts/ts-universal-attributes-reuse-id.md)来区分不同结构的复用组件。 24> 25 26## 限制条件 27 28- \@Reusable装饰器仅用于自定义组件。 29 30```ts 31import { ComponentContent } from "@kit.ArkUI"; 32 33// @Builder加上@Reusable编译报错,不适用于builder。 34// @Reusable 35@Builder 36function buildCreativeLoadingDialog(closedClick: () => void) { 37 Crash() 38} 39 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 let contentNode = new ComponentContent(this.uiContext, wrapBuilder(buildCreativeLoadingDialog), () => { 75 }); 76 this.uiContext.getPromptAction().openCustomDialog(contentNode); 77 }) 78 } 79 .height('100%') 80 .width('100%') 81 } 82} 83``` 84 85- 被@Reusable装饰的自定义组件在复用时,会递归调用该自定义组件及其所有子组件的aboutToReuse回调函数。若在子组件的aboutToReuse函数中修改了父组件的状态变量,此次修改将不会生效,请避免此类用法。若需设置父组件的状态变量,可使用setTimeout设置延迟执行,将任务抛出组件复用的作用范围,使修改生效。 86 87 88 【反例】 89 90 在子组件的aboutToReuse中,直接修改父组件的状态变量。 91 92 ```ts 93 class BasicDataSource implements IDataSource { 94 private listener: DataChangeListener | undefined = undefined; 95 public dataArray: number[] = []; 96 97 totalCount(): number { 98 return this.dataArray.length; 99 } 100 101 getData(index: number): number { 102 return this.dataArray[index]; 103 } 104 105 registerDataChangeListener(listener: DataChangeListener): void { 106 this.listener = listener; 107 } 108 109 unregisterDataChangeListener(listener: DataChangeListener): void { 110 this.listener = undefined; 111 } 112 } 113 114 @Entry 115 @Component 116 struct Index { 117 private data: BasicDataSource = new BasicDataSource(); 118 119 aboutToAppear(): void { 120 for (let index = 1; index < 20; index++) { 121 this.data.dataArray.push(index); 122 } 123 } 124 125 build() { 126 List() { 127 LazyForEach(this.data, (item: number, index: number) => { 128 ListItem() { 129 ReuseComponent({ num: item }) 130 } 131 }, (item: number, index: number) => index.toString()) 132 }.cachedCount(0) 133 } 134 } 135 136 @Reusable 137 @Component 138 struct ReuseComponent { 139 @State num: number = 0; 140 141 aboutToReuse(params: ESObject): void { 142 this.num = params.num; 143 } 144 145 build() { 146 Column() { 147 Text('ReuseComponent num:' + this.num.toString()) 148 ReuseComponentChild({ num: this.num }) 149 Button('plus') 150 .onClick(() => { 151 this.num += 10; 152 }) 153 } 154 .height(200) 155 } 156 } 157 158 @Component 159 struct ReuseComponentChild { 160 @Link num: number; 161 162 aboutToReuse(params: ESObject): void { 163 this.num = -1 * params.num; 164 } 165 166 build() { 167 Text('ReuseComponentChild num:' + this.num.toString()) 168 } 169 } 170 ``` 171 172 【正例】 173 174 在子组件的aboutToReuse中,使用setTimeout,将修改抛出组件复用的作用范围。 175 176 ```ts 177 class BasicDataSource implements IDataSource { 178 private listener: DataChangeListener | undefined = undefined; 179 public dataArray: number[] = []; 180 181 totalCount(): number { 182 return this.dataArray.length; 183 } 184 185 getData(index: number): number { 186 return this.dataArray[index]; 187 } 188 189 registerDataChangeListener(listener: DataChangeListener): void { 190 this.listener = listener; 191 } 192 193 unregisterDataChangeListener(listener: DataChangeListener): void { 194 this.listener = undefined; 195 } 196 } 197 198 @Entry 199 @Component 200 struct Index { 201 private data: BasicDataSource = new BasicDataSource(); 202 203 aboutToAppear(): void { 204 for (let index = 1; index < 20; index++) { 205 this.data.dataArray.push(index); 206 } 207 } 208 209 build() { 210 List() { 211 LazyForEach(this.data, (item: number, index: number) => { 212 ListItem() { 213 ReuseComponent({ num: item }) 214 } 215 }, (item: number, index: number) => index.toString()) 216 }.cachedCount(0) 217 } 218 } 219 220 @Reusable 221 @Component 222 struct ReuseComponent { 223 @State num: number = 0; 224 225 aboutToReuse(params: ESObject): void { 226 this.num = params.num; 227 } 228 229 build() { 230 Column() { 231 Text('ReuseComponent num:' + this.num.toString()) 232 ReuseComponentChild({ num: this.num }) 233 Button('plus') 234 .onClick(() => { 235 this.num += 10; 236 }) 237 } 238 .height(200) 239 } 240 } 241 242 @Component 243 struct ReuseComponentChild { 244 @Link num: number; 245 246 aboutToReuse(params: ESObject): void { 247 setTimeout(() => { 248 this.num = -1 * params.num; 249 }, 1) 250 } 251 252 build() { 253 Text('ReuseComponentChild num:' + this.num.toString()) 254 } 255 } 256 ``` 257 258- ComponentContent不支持传入\@Reusable装饰器装饰的自定义组件。 259 260```ts 261import { ComponentContent } from "@kit.ArkUI"; 262 263@Builder 264function buildCreativeLoadingDialog(closedClick: () => void) { 265 Crash() 266} 267 268// 如果注释掉就可以正常弹出弹窗,如果加上@Reusable就直接crash。 269@Reusable 270@Component 271export struct Crash { 272 build() { 273 Column() { 274 Text("Crash") 275 .fontSize(12) 276 .lineHeight(18) 277 .fontColor(Color.Blue) 278 .margin({ 279 left: 6 280 }) 281 }.width('100%') 282 .height('100%') 283 .justifyContent(FlexAlign.Center) 284 } 285} 286 287@Entry 288@Component 289struct Index { 290 @State message: string = 'Hello World'; 291 private uiContext = this.getUIContext(); 292 293 build() { 294 RelativeContainer() { 295 Text(this.message) 296 .id('Index') 297 .fontSize(50) 298 .fontWeight(FontWeight.Bold) 299 .alignRules({ 300 center: { anchor: '__container__', align: VerticalAlign.Center }, 301 middle: { anchor: '__container__', align: HorizontalAlign.Center } 302 }) 303 .onClick(() => { 304 // ComponentContent底层是BuilderNode,BuilderNode不支持传入@Reusable注解的自定义组件。 305 let contentNode = new ComponentContent(this.uiContext, wrapBuilder(buildCreativeLoadingDialog), () => { 306 }); 307 this.uiContext.getPromptAction().openCustomDialog(contentNode); 308 }) 309 } 310 .height('100%') 311 .width('100%') 312 } 313} 314``` 315 316- \@Reusable装饰器不建议嵌套使用,会增加内存,降低复用效率,加大维护难度。嵌套使用会导致额外缓存池的生成,各缓存池拥有相同树状结构,复用效率低下。此外,嵌套使用会使生命周期管理复杂,资源和变量共享困难。 317 318 319## 使用场景 320 321### 动态布局更新 322 323重复创建与移除视图可能引起频繁的布局计算,从而影响帧率。采用组件复用可以避免不必要的视图创建与布局计算,提升性能。 324以下示例中,将Child自定义组件标记为复用组件,通过Button点击更新Child,触发复用。 325 326```ts 327// xxx.ets 328export class Message { 329 value: string | undefined; 330 331 constructor(value: string) { 332 this.value = value; 333 } 334} 335 336@Entry 337@Component 338struct Index { 339 @State switch: boolean = true; 340 341 build() { 342 Column() { 343 Button('Hello') 344 .fontSize(30) 345 .fontWeight(FontWeight.Bold) 346 .onClick(() => { 347 this.switch = !this.switch; 348 }) 349 if (this.switch) { 350 // 如果只有一个复用的组件,可以不用设置reuseId。 351 Child({ message: new Message('Child') }) 352 .reuseId('Child') 353 } 354 } 355 .height("100%") 356 .width('100%') 357 } 358} 359 360@Reusable 361@Component 362struct Child { 363 @State message: Message = new Message('AboutToReuse'); 364 365 aboutToReuse(params: Record<string, ESObject>) { 366 console.info("Recycle====Child=="); 367 this.message = params.message as Message; 368 } 369 370 build() { 371 Column() { 372 Text(this.message.value) 373 .fontSize(30) 374 } 375 .borderWidth(1) 376 .height(100) 377 } 378} 379``` 380 381### 列表滚动配合LazyForEach使用 382 383- 当应用展示大量数据的列表并进行滚动操作时,频繁创建和销毁列表项视图可能导致卡顿和性能问题。使用列表组件的组件复用机制可以重用已创建的列表项视图,提高滚动流畅度。 384 385- 以下示例代码将CardView自定义组件标记为复用组件,List上下滑动,触发CardView复用。 386 387```ts 388class MyDataSource implements IDataSource { 389 private dataArray: string[] = []; 390 private listener: DataChangeListener | undefined; 391 392 public totalCount(): number { 393 return this.dataArray.length; 394 } 395 396 public getData(index: number): string { 397 return this.dataArray[index]; 398 } 399 400 public pushData(data: string): void { 401 this.dataArray.push(data); 402 } 403 404 public reloadListener(): void { 405 this.listener?.onDataReloaded(); 406 } 407 408 public registerDataChangeListener(listener: DataChangeListener): void { 409 this.listener = listener; 410 } 411 412 public unregisterDataChangeListener(listener: DataChangeListener): void { 413 this.listener = undefined; 414 } 415} 416 417@Entry 418@Component 419struct ReuseDemo { 420 private data: MyDataSource = new MyDataSource(); 421 422 aboutToAppear() { 423 for (let i = 1; i < 1000; i++) { 424 this.data.pushData(i + ""); 425 } 426 } 427 428 // ... 429 build() { 430 Column() { 431 List() { 432 LazyForEach(this.data, (item: string) => { 433 ListItem() { 434 CardView({ item: item }) 435 } 436 }, (item: string) => item) 437 } 438 } 439 } 440} 441 442// 复用组件 443@Reusable 444@Component 445export struct CardView { 446 // 被\@State修饰的变量item才能更新,未被\@State修饰的变量不会更新。 447 @State item: string = ''; 448 449 aboutToReuse(params: Record<string, Object>): void { 450 this.item = params.item as string; 451 } 452 453 build() { 454 Column() { 455 Text(this.item) 456 .fontSize(30) 457 } 458 .borderWidth(1) 459 .height(100) 460 } 461} 462``` 463 464### 列表滚动-if使用场景 465 466以下示例代码将OneMoment自定义组件标记为复用组件。当List上下滑动时,会触发OneMoment的复用。设置reuseId可为复用组件分配复用组,相同reuseId的组件将在同一复用组中复用。单个复用组件无需设置reuseId。使用reuseId标识复用组件,可避免重复执行if语句的删除和重新创建逻辑,提高复用效率和性能。 467 468```ts 469@Entry 470@Component 471struct Index { 472 private dataSource = new MyDataSource<FriendMoment>(); 473 474 aboutToAppear(): void { 475 for (let i = 0; i < 20; i++) { 476 let title = i + 1 + "test_if"; 477 this.dataSource.pushData(new FriendMoment(i.toString(), title, 'app.media.app_icon')); 478 } 479 480 for (let i = 0; i < 50; i++) { 481 let title = i + 1 + "test_if"; 482 this.dataSource.pushData(new FriendMoment(i.toString(), title, '')); 483 } 484 } 485 486 build() { 487 Column() { 488 // TopBar() 489 List({ space: 3 }) { 490 LazyForEach(this.dataSource, (moment: FriendMoment) => { 491 ListItem() { 492 // 使用reuseId进行组件复用的控制。 493 OneMoment({ moment: moment }) 494 .reuseId((moment.image !== '') ? 'withImage' : 'noImage') 495 } 496 }, (moment: FriendMoment) => moment.id) 497 } 498 .cachedCount(0) 499 } 500 } 501} 502 503class FriendMoment { 504 id: string = ''; 505 text: string = ''; 506 title: string = ''; 507 image: string = ''; 508 answers: Array<ResourceStr> = []; 509 510 constructor(id: string, title: string, image: string) { 511 this.text = id; 512 this.title = title; 513 this.image = image; 514 } 515} 516 517@Reusable 518@Component 519export struct OneMoment { 520 @Prop moment: FriendMoment; 521 522 // 复用id相同的组件才能触发复用。 523 aboutToReuse(params: ESObject): void { 524 console.log("=====aboutToReuse====OneMoment==复用了==" + this.moment.text); 525 } 526 527 build() { 528 Column() { 529 Text(this.moment.text) 530 // if分支判断。 531 if (this.moment.image !== '') { 532 Flex({ wrap: FlexWrap.Wrap }) { 533 Image($r(this.moment.image)).height(50).width(50) 534 Image($r(this.moment.image)).height(50).width(50) 535 Image($r(this.moment.image)).height(50).width(50) 536 Image($r(this.moment.image)).height(50).width(50) 537 } 538 } 539 } 540 } 541} 542 543class BasicDataSource<T> implements IDataSource { 544 private listeners: DataChangeListener[] = []; 545 private originDataArray: T[] = []; 546 547 public totalCount(): number { 548 return 0; 549 } 550 551 public getData(index: number): T { 552 return this.originDataArray[index]; 553 } 554 555 registerDataChangeListener(listener: DataChangeListener): void { 556 if (this.listeners.indexOf(listener) < 0) { 557 this.listeners.push(listener); 558 } 559 } 560 561 unregisterDataChangeListener(listener: DataChangeListener): void { 562 const pos = this.listeners.indexOf(listener); 563 if (pos >= 0) { 564 this.listeners.splice(pos, 1); 565 } 566 } 567 568 notifyDataAdd(index: number): void { 569 this.listeners.forEach(listener => { 570 listener.onDataAdd(index); 571 }); 572 } 573} 574 575export class MyDataSource<T> extends BasicDataSource<T> { 576 private dataArray: T[] = []; 577 578 public totalCount(): number { 579 return this.dataArray.length; 580 } 581 582 public getData(index: number): T { 583 return this.dataArray[index]; 584 } 585 586 public pushData(data: T): void { 587 this.dataArray.push(data); 588 this.notifyDataAdd(this.dataArray.length - 1); 589 } 590} 591``` 592 593### 列表滚动-Foreach使用场景 594 595使用Foreach创建可复用的自定义组件,由于Foreach渲染控制语法的全展开属性,导致复用组件无法复用。示例中点击update,数据刷新成功,但滑动列表时,ListItemView无法复用。点击clear,再次点击update,ListItemView复用成功,因为一帧内重复创建多个已被销毁的自定义组件。 596 597```ts 598// xxx.ets 599class MyDataSource implements IDataSource { 600 private dataArray: string[] = []; 601 602 public totalCount(): number { 603 return this.dataArray.length; 604 } 605 606 public getData(index: number): string { 607 return this.dataArray[index]; 608 } 609 610 public pushData(data: string): void { 611 this.dataArray.push(data); 612 } 613 614 public registerDataChangeListener(listener: DataChangeListener): void { 615 } 616 617 public unregisterDataChangeListener(listener: DataChangeListener): void { 618 } 619} 620 621@Entry 622@Component 623struct Index { 624 private data: MyDataSource = new MyDataSource(); 625 private data02: MyDataSource = new MyDataSource(); 626 @State isShow: boolean = true; 627 @State dataSource: ListItemObject[] = []; 628 629 aboutToAppear() { 630 for (let i = 0; i < 100; i++) { 631 this.data.pushData(i.toString()); 632 } 633 634 for (let i = 30; i < 80; i++) { 635 this.data02.pushData(i.toString()); 636 } 637 } 638 639 build() { 640 Column() { 641 Row() { 642 Button('clear').onClick(() => { 643 for (let i = 1; i < 50; i++) { 644 this.dataSource.pop(); 645 } 646 }).height(40) 647 648 Button('update').onClick(() => { 649 for (let i = 1; i < 50; i++) { 650 let obj = new ListItemObject(); 651 obj.id = i; 652 obj.uuid = Math.random().toString(); 653 obj.isExpand = false; 654 this.dataSource.push(obj); 655 } 656 }).height(40) 657 } 658 659 List({ space: 10 }) { 660 ForEach(this.dataSource, (item: ListItemObject) => { 661 ListItem() { 662 ListItemView({ 663 obj: item 664 }) 665 } 666 }, (item: ListItemObject) => { 667 return item.uuid.toString(); 668 }) 669 670 }.cachedCount(0) 671 .width('100%') 672 .height('100%') 673 } 674 } 675} 676 677@Reusable 678@Component 679struct ListItemView { 680 @ObjectLink obj: ListItemObject; 681 @State item: string = ''; 682 683 aboutToAppear(): void { 684 // 点击 update,首次进入,上下滑动,由于Foreach折叠展开属性,无法复用。 685 console.log("=====aboutToAppear=====ListItemView==创建了==" + this.item); 686 } 687 688 aboutToReuse(params: ESObject) { 689 this.item = params.item; 690 // 点击clear,再次update,复用成功。 691 // 符合一帧内重复创建多个已被销毁的自定义组件。 692 console.log("=====aboutToReuse====ListItemView==复用了==" + this.item); 693 } 694 695 build() { 696 Column({ space: 10 }) { 697 Text(`${this.obj.id}.标题`) 698 .fontSize(16) 699 .fontColor('#000000') 700 .padding({ 701 top: 20, 702 bottom: 20, 703 }) 704 705 if (this.obj.isExpand) { 706 Text('') 707 .fontSize(14) 708 .fontColor('#999999') 709 } 710 } 711 .width('100%') 712 .borderRadius(10) 713 .backgroundColor(Color.White) 714 .padding(15) 715 .onClick(() => { 716 this.obj.isExpand = !this.obj.isExpand; 717 }) 718 } 719} 720 721@Observed 722class ListItemObject { 723 uuid: string = ""; 724 id: number = 0; 725 isExpand: boolean = false; 726} 727``` 728 729### Grid使用场景 730 731示例中使用\@Reusable装饰器修饰GridItem中的自定义组件ReusableChildComponent,即表示其具备组件复用的能力。 732使用aboutToReuse可以在 Grid 滑动时,从复用缓存中加入到组件树之前触发,从而更新组件状态变量,展示正确内容。 733需要注意的是无需在aboutToReuse中对[\@Link](arkts-link.md)、[\@StorageLink](arkts-appstorage.md#storagelink)、[\@ObjectLink](arkts-observed-and-objectlink.md)、[\@Consume](arkts-provide-and-consume.md)等自动更新值的状态变量进行更新,可能触发不必要的组件刷新。 734 735```ts 736// MyDataSource类实现IDataSource接口。 737class MyDataSource implements IDataSource { 738 private dataArray: number[] = []; 739 740 public pushData(data: number): void { 741 this.dataArray.push(data); 742 } 743 744 // 数据源的数据总量。 745 public totalCount(): number { 746 return this.dataArray.length; 747 } 748 749 // 返回指定索引位置的数据。 750 public getData(index: number): number { 751 return this.dataArray[index]; 752 } 753 754 registerDataChangeListener(listener: DataChangeListener): void { 755 } 756 757 unregisterDataChangeListener(listener: DataChangeListener): void { 758 } 759} 760 761@Entry 762@Component 763struct MyComponent { 764 // 数据源。 765 private data: MyDataSource = new MyDataSource(); 766 767 aboutToAppear() { 768 for (let i = 1; i < 1000; i++) { 769 this.data.pushData(i); 770 } 771 } 772 773 build() { 774 Column({ space: 5 }) { 775 Grid() { 776 LazyForEach(this.data, (item: number) => { 777 GridItem() { 778 // 使用可复用自定义组件。 779 ReusableChildComponent({ item: item }) 780 } 781 }, (item: string) => item) 782 } 783 .cachedCount(2) // 设置GridItem的缓存数量。 784 .columnsTemplate('1fr 1fr 1fr') 785 .columnsGap(10) 786 .rowsGap(10) 787 .margin(10) 788 .height(500) 789 .backgroundColor(0xFAEEE0) 790 } 791 } 792} 793 794@Reusable 795@Component 796struct ReusableChildComponent { 797 @State item: number = 0; 798 799 // aboutToReuse从复用缓存中加入到组件树之前调用,可在此处更新组件的状态变量以展示正确的内容。 800 // aboutToReuse参数类型已不支持any,这里使用Record指定明确的数据类型。Record用于构造一个对象类型,其属性键为Keys,属性值为Type。 801 aboutToReuse(params: Record<string, number>) { 802 this.item = params.item; 803 } 804 805 build() { 806 Column() { 807 // 请开发者自行在src/main/resources/base/media路径下添加app.media.app_icon图片,否则运行时会因资源缺失而报错。 808 Image($r('app.media.app_icon')) 809 .objectFit(ImageFit.Fill) 810 .layoutWeight(1) 811 Text(`图片${this.item}`) 812 .fontSize(16) 813 .textAlign(TextAlign.Center) 814 } 815 .width('100%') 816 .height(120) 817 .backgroundColor(0xF9CF93) 818 } 819} 820``` 821 822### WaterFlow使用场景 823 824- 在WaterFlow滑动场景中,FlowItem及其子组件频繁创建和销毁。可以将FlowItem中的组件封装成自定义组件,并使用\@Reusable装饰器修饰,实现组件复用。 825 826```ts 827class WaterFlowDataSource implements IDataSource { 828 private dataArray: number[] = []; 829 private listeners: DataChangeListener[] = []; 830 831 constructor() { 832 for (let i = 0; i <= 60; i++) { 833 this.dataArray.push(i); 834 } 835 } 836 837 // 获取索引对应的数据。 838 public getData(index: number): number { 839 return this.dataArray[index]; 840 } 841 842 // 通知控制器增加数据。 843 notifyDataAdd(index: number): void { 844 this.listeners.forEach(listener => { 845 listener.onDataAdd(index); 846 }); 847 } 848 849 // 获取数据总数。 850 public totalCount(): number { 851 return this.dataArray.length; 852 } 853 854 // 注册改变数据的控制器。 855 registerDataChangeListener(listener: DataChangeListener): void { 856 if (this.listeners.indexOf(listener) < 0) { 857 this.listeners.push(listener); 858 } 859 } 860 861 // 注销改变数据的控制器。 862 unregisterDataChangeListener(listener: DataChangeListener): void { 863 const pos = this.listeners.indexOf(listener); 864 if (pos >= 0) { 865 this.listeners.splice(pos, 1); 866 } 867 } 868 869 // 在数据尾部增加一个元素。 870 public addLastItem(): void { 871 this.dataArray.splice(this.dataArray.length, 0, this.dataArray.length); 872 this.notifyDataAdd(this.dataArray.length - 1); 873 } 874} 875 876@Reusable 877@Component 878struct ReusableFlowItem { 879 @State item: number = 0; 880 881 // 从复用缓存中加入到组件树之前调用,可在此处更新组件的状态变量以展示正确的内容。 882 aboutToReuse(params: ESObject) { 883 this.item = params.item; 884 console.log("=====aboutToReuse====FlowItem==复用了==" + this.item); 885 } 886 887 aboutToRecycle(): void { 888 console.log("=====aboutToRecycle====FlowItem==回收了==" + this.item); 889 } 890 891 build() { 892 // 请开发者自行在src/main/resources/base/media路径下添加app.media.app_icon图片,否则运行时会因资源缺失而报错。 893 Column() { 894 Text("N" + this.item).fontSize(24).height('26').margin(10) 895 Image($r('app.media.app_icon')) 896 .objectFit(ImageFit.Cover) 897 .width(50) 898 .height(50) 899 } 900 } 901} 902 903@Entry 904@Component 905struct Index { 906 @State minSize: number = 50; 907 @State maxSize: number = 80; 908 @State fontSize: number = 24; 909 @State colors: number[] = [0xFFC0CB, 0xDA70D6, 0x6B8E23, 0x6A5ACD, 0x00FFFF, 0x00FF7F]; 910 scroller: Scroller = new Scroller(); 911 dataSource: WaterFlowDataSource = new WaterFlowDataSource(); 912 private itemWidthArray: number[] = []; 913 private itemHeightArray: number[] = []; 914 915 // 计算flow item宽/高。 916 getSize() { 917 let ret = Math.floor(Math.random() * this.maxSize); 918 return (ret > this.minSize ? ret : this.minSize); 919 } 920 921 // 保存flow item宽/高。 922 getItemSizeArray() { 923 for (let i = 0; i < 100; i++) { 924 this.itemWidthArray.push(this.getSize()); 925 this.itemHeightArray.push(this.getSize()); 926 } 927 } 928 929 aboutToAppear() { 930 this.getItemSizeArray(); 931 } 932 933 build() { 934 Stack({ alignContent: Alignment.TopStart }) { 935 Column({ space: 2 }) { 936 Button('back top') 937 .height('5%') 938 .onClick(() => { 939 940 // 点击后回到顶部。 941 this.scroller.scrollEdge(Edge.Top); 942 }) 943 WaterFlow({ scroller: this.scroller }) { 944 LazyForEach(this.dataSource, (item: number) => { 945 FlowItem() { 946 ReusableFlowItem({ item: item }) 947 }.onAppear(() => { 948 if (item + 20 == this.dataSource.totalCount()) { 949 for (let i = 0; i < 50; i++) { 950 this.dataSource.addLastItem(); 951 } 952 } 953 }) 954 955 }) 956 } 957 } 958 } 959 } 960} 961``` 962 963### Swiper使用场景 964 965- 在Swiper滑动场景中,条目中的子组件频繁创建和销毁。可以将这些子组件封装成自定义组件,并使用\@Reusable装饰器修饰,以实现组件复用。 966 967```ts 968@Entry 969@Component 970struct Index { 971 private dataSource = new MyDataSource<Question>(); 972 973 aboutToAppear(): void { 974 for (let i = 0; i < 1000; i++) { 975 let title = i + 1 + "test_swiper"; 976 let answers = ["test1", "test2", "test3", 977 "test4"]; 978 // 请开发者自行在src/main/resources/base/media路径下添加app.media.app_icon图片,否则运行时会因资源缺失而报错。 979 this.dataSource.pushData(new Question(i.toString(), title, $r('app.media.app_icon'), answers)); 980 } 981 } 982 983 build() { 984 Column({ space: 5 }) { 985 Swiper() { 986 LazyForEach(this.dataSource, (item: Question) => { 987 QuestionSwiperItem({ itemData: item }) 988 }, (item: Question) => item.id) 989 } 990 } 991 .width('100%') 992 .margin({ top: 5 }) 993 } 994} 995 996class Question { 997 id: string = ''; 998 title: ResourceStr = ''; 999 image: ResourceStr = ''; 1000 answers: Array<ResourceStr> = []; 1001 1002 constructor(id: string, title: ResourceStr, image: ResourceStr, answers: Array<ResourceStr>) { 1003 this.id = id; 1004 this.title = title; 1005 this.image = image; 1006 this.answers = answers; 1007 } 1008} 1009 1010@Reusable 1011@Component 1012struct QuestionSwiperItem { 1013 @State itemData: Question | null = null; 1014 1015 aboutToReuse(params: Record<string, Object>): void { 1016 this.itemData = params.itemData as Question; 1017 console.info("===aboutToReuse====QuestionSwiperItem=="); 1018 } 1019 1020 build() { 1021 Column() { 1022 Text(this.itemData?.title) 1023 .fontSize(18) 1024 .fontColor($r('sys.color.ohos_id_color_primary')) 1025 .alignSelf(ItemAlign.Start) 1026 .margin({ 1027 top: 10, 1028 bottom: 16 1029 }) 1030 Image(this.itemData?.image) 1031 .width('100%') 1032 .borderRadius(12) 1033 .objectFit(ImageFit.Contain) 1034 .margin({ 1035 bottom: 16 1036 }) 1037 .height(80) 1038 .width(80) 1039 1040 Column({ space: 16 }) { 1041 ForEach(this.itemData?.answers, (item: Resource) => { 1042 Text(item) 1043 .fontSize(16) 1044 .fontColor($r('sys.color.ohos_id_color_primary')) 1045 }, (item: ResourceStr) => JSON.stringify(item)) 1046 } 1047 .width('100%') 1048 .alignItems(HorizontalAlign.Start) 1049 } 1050 .width('100%') 1051 .padding({ 1052 left: 16, 1053 right: 16 1054 }) 1055 } 1056} 1057 1058class BasicDataSource<T> implements IDataSource { 1059 private listeners: DataChangeListener[] = []; 1060 private originDataArray: T[] = []; 1061 1062 public totalCount(): number { 1063 return 0; 1064 } 1065 1066 public getData(index: number): T { 1067 return this.originDataArray[index]; 1068 } 1069 1070 registerDataChangeListener(listener: DataChangeListener): void { 1071 if (this.listeners.indexOf(listener) < 0) { 1072 this.listeners.push(listener); 1073 } 1074 } 1075 1076 unregisterDataChangeListener(listener: DataChangeListener): void { 1077 const pos = this.listeners.indexOf(listener); 1078 if (pos >= 0) { 1079 this.listeners.splice(pos, 1); 1080 } 1081 } 1082 1083 notifyDataAdd(index: number): void { 1084 this.listeners.forEach(listener => { 1085 listener.onDataAdd(index); 1086 }); 1087 } 1088} 1089 1090export class MyDataSource<T> extends BasicDataSource<T> { 1091 private dataArray: T[] = []; 1092 1093 public totalCount(): number { 1094 return this.dataArray.length; 1095 } 1096 1097 public getData(index: number): T { 1098 return this.dataArray[index]; 1099 } 1100 1101 public pushData(data: T): void { 1102 this.dataArray.push(data); 1103 this.notifyDataAdd(this.dataArray.length - 1); 1104 } 1105} 1106``` 1107 1108### 列表滚动-ListItemGroup使用场景 1109 1110- 可以视作特殊List滑动场景,将ListItem需要移除重建的子组件封装成自定义组件,并使用\@Reusable装饰器修饰,使其具备组件复用能力。 1111 1112```ts 1113@Entry 1114@Component 1115struct ListItemGroupAndReusable { 1116 data: DataSrc2 = new DataSrc2(); 1117 1118 @Builder 1119 itemHead(text: string) { 1120 Text(text) 1121 .fontSize(20) 1122 .backgroundColor(0xAABBCC) 1123 .width('100%') 1124 .padding(10) 1125 } 1126 1127 aboutToAppear() { 1128 for (let i = 0; i < 10000; i++) { 1129 let data_1 = new DataSrc1(); 1130 for (let j = 0; j < 12; j++) { 1131 data_1.Data.push(`测试条目数据: ${i} - ${j}`); 1132 } 1133 this.data.Data.push(data_1); 1134 } 1135 } 1136 1137 build() { 1138 Stack() { 1139 List() { 1140 LazyForEach(this.data, (item: DataSrc1, index: number) => { 1141 ListItemGroup({ header: this.itemHead(index.toString()) }) { 1142 LazyForEach(item, (ii: string, index: number) => { 1143 ListItem() { 1144 Inner({ str: ii }) 1145 } 1146 }) 1147 } 1148 .width('100%') 1149 .height('60vp') 1150 }) 1151 } 1152 } 1153 .width('100%') 1154 .height('100%') 1155 } 1156} 1157 1158@Reusable 1159@Component 1160struct Inner { 1161 @State str: string = ''; 1162 1163 aboutToReuse(param: ESObject) { 1164 this.str = param.str; 1165 } 1166 1167 build() { 1168 Text(this.str) 1169 } 1170} 1171 1172class DataSrc1 implements IDataSource { 1173 listeners: DataChangeListener[] = []; 1174 Data: string[] = []; 1175 1176 public totalCount(): number { 1177 return this.Data.length; 1178 } 1179 1180 public getData(index: number): string { 1181 return this.Data[index]; 1182 } 1183 1184 // 该方法为框架侧调用,为LazyForEach组件向其数据源处添加listener监听。 1185 registerDataChangeListener(listener: DataChangeListener): void { 1186 if (this.listeners.indexOf(listener) < 0) { 1187 this.listeners.push(listener); 1188 } 1189 } 1190 1191 // 该方法为框架侧调用,为对应的LazyForEach组件在数据源处去除listener监听。 1192 unregisterDataChangeListener(listener: DataChangeListener): void { 1193 const pos = this.listeners.indexOf(listener); 1194 if (pos >= 0) { 1195 this.listeners.splice(pos, 1); 1196 } 1197 } 1198 1199 // 通知LazyForEach组件需要重载所有子组件。 1200 notifyDataReload(): void { 1201 this.listeners.forEach(listener => { 1202 listener.onDataReloaded(); 1203 }); 1204 } 1205 1206 // 通知LazyForEach组件需要在index对应索引处添加子组件。 1207 notifyDataAdd(index: number): void { 1208 this.listeners.forEach(listener => { 1209 listener.onDataAdd(index); 1210 }); 1211 } 1212 1213 // 通知LazyForEach组件在index对应索引处数据有变化,需要重建该子组件。 1214 notifyDataChange(index: number): void { 1215 this.listeners.forEach(listener => { 1216 listener.onDataChange(index); 1217 }); 1218 } 1219 1220 // 通知LazyForEach组件需要在index对应索引处删除该子组件。 1221 notifyDataDelete(index: number): void { 1222 this.listeners.forEach(listener => { 1223 listener.onDataDelete(index); 1224 }); 1225 } 1226 1227 // 通知LazyForEach组件将from索引和to索引处的子组件进行交换。 1228 notifyDataMove(from: number, to: number): void { 1229 this.listeners.forEach(listener => { 1230 listener.onDataMove(from, to); 1231 }); 1232 } 1233} 1234 1235class DataSrc2 implements IDataSource { 1236 listeners: DataChangeListener[] = []; 1237 Data: DataSrc1[] = []; 1238 1239 public totalCount(): number { 1240 return this.Data.length; 1241 } 1242 1243 public getData(index: number): DataSrc1 { 1244 return this.Data[index]; 1245 } 1246 1247 // 该方法为框架侧调用,为LazyForEach组件向其数据源处添加listener监听。 1248 registerDataChangeListener(listener: DataChangeListener): void { 1249 if (this.listeners.indexOf(listener) < 0) { 1250 this.listeners.push(listener); 1251 } 1252 } 1253 1254 // 该方法为框架侧调用,为对应的LazyForEach组件在数据源处去除listener监听。 1255 unregisterDataChangeListener(listener: DataChangeListener): void { 1256 const pos = this.listeners.indexOf(listener); 1257 if (pos >= 0) { 1258 this.listeners.splice(pos, 1); 1259 } 1260 } 1261 1262 // 通知LazyForEach组件需要重载所有子组件。 1263 notifyDataReload(): void { 1264 this.listeners.forEach(listener => { 1265 listener.onDataReloaded(); 1266 }); 1267 } 1268 1269 // 通知LazyForEach组件需要在index对应索引处添加子组件。 1270 notifyDataAdd(index: number): void { 1271 this.listeners.forEach(listener => { 1272 listener.onDataAdd(index); 1273 }); 1274 } 1275 1276 // 通知LazyForEach组件在index对应索引处数据有变化,需要重建该子组件。 1277 notifyDataChange(index: number): void { 1278 this.listeners.forEach(listener => { 1279 listener.onDataChange(index); 1280 }); 1281 } 1282 1283 // 通知LazyForEach组件需要在index对应索引处删除该子组件。 1284 notifyDataDelete(index: number): void { 1285 this.listeners.forEach(listener => { 1286 listener.onDataDelete(index); 1287 }); 1288 } 1289 1290 // 通知LazyForEach组件将from索引和to索引处的子组件进行交换。 1291 notifyDataMove(from: number, to: number): void { 1292 this.listeners.forEach(listener => { 1293 listener.onDataMove(from, to); 1294 }); 1295 } 1296} 1297``` 1298 1299 1300### 多种条目类型使用场景 1301 1302**标准型** 1303 1304复用组件的布局相同,示例参见本文列表滚动部分的描述。 1305 1306**有限变化型** 1307 1308复用组件间存在差异,但类型有限。例如,可以通过显式设置两个reuseId或使用两个自定义组件来实现复用。 1309 1310```ts 1311class MyDataSource implements IDataSource { 1312 private dataArray: string[] = []; 1313 private listener: DataChangeListener | undefined; 1314 1315 public totalCount(): number { 1316 return this.dataArray.length; 1317 } 1318 1319 public getData(index: number): string { 1320 return this.dataArray[index]; 1321 } 1322 1323 public pushData(data: string): void { 1324 this.dataArray.push(data); 1325 } 1326 1327 public reloadListener(): void { 1328 this.listener?.onDataReloaded(); 1329 } 1330 1331 public registerDataChangeListener(listener: DataChangeListener): void { 1332 this.listener = listener; 1333 } 1334 1335 public unregisterDataChangeListener(listener: DataChangeListener): void { 1336 this.listener = undefined; 1337 } 1338} 1339 1340@Entry 1341@Component 1342struct Index { 1343 private data: MyDataSource = new MyDataSource(); 1344 1345 aboutToAppear() { 1346 for (let i = 0; i < 1000; i++) { 1347 this.data.pushData(i + ""); 1348 } 1349 } 1350 1351 build() { 1352 Column() { 1353 List({ space: 10 }) { 1354 LazyForEach(this.data, (item: number) => { 1355 ListItem() { 1356 ReusableComponent({ item: item }) 1357 // 设置两种有限变化的reuseId 1358 .reuseId(item % 2 === 0 ? 'ReusableComponentOne' : 'ReusableComponentTwo') 1359 } 1360 .backgroundColor(Color.Orange) 1361 .width('100%') 1362 }, (item: number) => item.toString()) 1363 } 1364 .cachedCount(2) 1365 } 1366 } 1367} 1368 1369@Reusable 1370@Component 1371struct ReusableComponent { 1372 @State item: number = 0; 1373 1374 aboutToReuse(params: ESObject) { 1375 this.item = params.item; 1376 } 1377 1378 build() { 1379 Column() { 1380 // 组件内部根据类型差异渲染 1381 if (this.item % 2 === 0) { 1382 Text(`Item ${this.item} ReusableComponentOne`) 1383 .fontSize(20) 1384 .margin({ left: 10 }) 1385 } else { 1386 Text(`Item ${this.item} ReusableComponentTwo`) 1387 .fontSize(20) 1388 .margin({ left: 10 }) 1389 } 1390 }.margin({ left: 10, right: 10 }) 1391 } 1392} 1393``` 1394 1395**组合型** 1396 1397复用组件间存在多种差异,但通常具备共同的子组件。将三种复用组件以组合型方式转换为Builder函数后,内部的共享子组件将统一置于父组件MyComponent之下。复用这些子组件时,缓存池在父组件层面实现共享,减少组件创建过程中的资源消耗。 1398 1399```ts 1400class MyDataSource implements IDataSource { 1401 private dataArray: string[] = []; 1402 private listener: DataChangeListener | undefined; 1403 1404 public totalCount(): number { 1405 return this.dataArray.length; 1406 } 1407 1408 public getData(index: number): string { 1409 return this.dataArray[index]; 1410 } 1411 1412 public pushData(data: string): void { 1413 this.dataArray.push(data); 1414 } 1415 1416 public reloadListener(): void { 1417 this.listener?.onDataReloaded(); 1418 } 1419 1420 public registerDataChangeListener(listener: DataChangeListener): void { 1421 this.listener = listener; 1422 } 1423 1424 public unregisterDataChangeListener(listener: DataChangeListener): void { 1425 this.listener = undefined; 1426 } 1427} 1428 1429@Entry 1430@Component 1431struct MyComponent { 1432 private data: MyDataSource = new MyDataSource(); 1433 1434 aboutToAppear() { 1435 for (let i = 0; i < 1000; i++) { 1436 this.data.pushData(i.toString()); 1437 } 1438 } 1439 1440 // itemBuilderOne作为复用组件的写法未展示,以下为转为Builder之后的写法。 1441 @Builder 1442 itemBuilderOne(item: string) { 1443 Column() { 1444 ChildComponentA({ item: item }) 1445 ChildComponentB({ item: item }) 1446 ChildComponentC({ item: item }) 1447 } 1448 } 1449 1450 // itemBuilderTwo转为Builder之后的写法。 1451 @Builder 1452 itemBuilderTwo(item: string) { 1453 Column() { 1454 ChildComponentA({ item: item }) 1455 ChildComponentC({ item: item }) 1456 ChildComponentD({ item: item }) 1457 } 1458 } 1459 1460 // itemBuilderThree转为Builder之后的写法。 1461 @Builder 1462 itemBuilderThree(item: string) { 1463 Column() { 1464 ChildComponentA({ item: item }) 1465 ChildComponentB({ item: item }) 1466 ChildComponentD({ item: item }) 1467 } 1468 } 1469 1470 build() { 1471 List({ space: 40 }) { 1472 LazyForEach(this.data, (item: string, index: number) => { 1473 ListItem() { 1474 if (index % 3 === 0) { 1475 this.itemBuilderOne(item) 1476 } else if (index % 5 === 0) { 1477 this.itemBuilderTwo(item) 1478 } else { 1479 this.itemBuilderThree(item) 1480 } 1481 } 1482 .backgroundColor('#cccccc') 1483 .width('100%') 1484 .onAppear(() => { 1485 console.log(`ListItem ${index} onAppear`); 1486 }) 1487 }, (item: number) => item.toString()) 1488 } 1489 .width('100%') 1490 .height('100%') 1491 .cachedCount(0) 1492 } 1493} 1494 1495@Reusable 1496@Component 1497struct ChildComponentA { 1498 @State item: string = ''; 1499 1500 aboutToReuse(params: ESObject) { 1501 console.log(`ChildComponentA ${params.item} Reuse ${this.item}`); 1502 this.item = params.item; 1503 } 1504 1505 aboutToRecycle(): void { 1506 console.log(`ChildComponentA ${this.item} Recycle`); 1507 } 1508 1509 build() { 1510 Column() { 1511 Text(`Item ${this.item} Child Component A`) 1512 .fontSize(20) 1513 .margin({ left: 10 }) 1514 .fontColor(Color.Blue) 1515 Grid() { 1516 ForEach((new Array(20)).fill(''), (item: string, index: number) => { 1517 GridItem() { 1518 // 请开发者自行在src/main/resources/base/media路径下添加app.media.startIcon图片,否则运行时会因资源缺失而报错。 1519 Image($r('app.media.startIcon')) 1520 .height(20) 1521 } 1522 }) 1523 } 1524 .columnsTemplate('1fr 1fr 1fr 1fr 1fr') 1525 .rowsTemplate('1fr 1fr 1fr 1fr') 1526 .columnsGap(10) 1527 .width('90%') 1528 .height(160) 1529 } 1530 .margin({ left: 10, right: 10 }) 1531 .backgroundColor(0xFAEEE0) 1532 } 1533} 1534 1535@Reusable 1536@Component 1537struct ChildComponentB { 1538 @State item: string = ''; 1539 1540 aboutToReuse(params: ESObject) { 1541 this.item = params.item; 1542 } 1543 1544 build() { 1545 Row() { 1546 Text(`Item ${this.item} Child Component B`) 1547 .fontSize(20) 1548 .margin({ left: 10 }) 1549 .fontColor(Color.Red) 1550 }.margin({ left: 10, right: 10 }) 1551 } 1552} 1553 1554@Reusable 1555@Component 1556struct ChildComponentC { 1557 @State item: string = ''; 1558 1559 aboutToReuse(params: ESObject) { 1560 this.item = params.item; 1561 } 1562 1563 build() { 1564 Row() { 1565 Text(`Item ${this.item} Child Component C`) 1566 .fontSize(20) 1567 .margin({ left: 10 }) 1568 .fontColor(Color.Green) 1569 }.margin({ left: 10, right: 10 }) 1570 } 1571} 1572 1573@Reusable 1574@Component 1575struct ChildComponentD { 1576 @State item: string = ''; 1577 1578 aboutToReuse(params: ESObject) { 1579 this.item = params.item; 1580 } 1581 1582 build() { 1583 Row() { 1584 Text(`Item ${this.item} Child Component D`) 1585 .fontSize(20) 1586 .margin({ left: 10 }) 1587 .fontColor(Color.Orange) 1588 }.margin({ left: 10, right: 10 }) 1589 } 1590} 1591``` 1592