# \@Reusable装饰器:组件复用 \@Reusable装饰器标记的自定义组件支持视图节点、组件实例和状态上下文的复用,避免重复创建和销毁,提升性能。 ## 概述 使用\@Reusable装饰器时,表示该自定义组件可以复用。与[\@Component装饰器](arkts-create-custom-components.md#component)结合使用,标记为\@Reusable的自定义组件在从组件树中移除时,组件及其对应的JS对象将被放入复用缓存中。后续创建新自定义组件节点时,将复用缓存中的节点,从而节约组件重新创建的时间。 > **说明:** > > API version 10开始支持@Reusable,支持在ArkTS中使用。 > > 关于组件复用的原理与使用、优化方法、适用场景,请参考最佳实践[组件复用最佳实践](https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-component-reuse)。 > > \@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)方法,因此,开发者在实现复用时,大部分代码都集中在这两个生命周期方法中。 > > 如果一个组件里可复用的组件不止一个,可以使用[reuseId](../../../application-dev/reference/apis-arkui/arkui-ts/ts-universal-attributes-reuse-id.md)来区分不同结构的复用组件。 > ## 限制条件 - \@Reusable装饰器仅用于自定义组件。 ```ts import { ComponentContent } from "@kit.ArkUI"; // @Builder加上@Reusable编译报错,不适用于builder。 // @Reusable @Builder function buildCreativeLoadingDialog(closedClick: () => void) { Crash() } @Component export struct Crash { build() { Column() { Text("Crash") .fontSize(12) .lineHeight(18) .fontColor(Color.Blue) .margin({ left: 6 }) }.width('100%') .height('100%') .justifyContent(FlexAlign.Center) } } @Entry @Component struct Index { @State message: string = 'Hello World'; private uiContext = this.getUIContext(); build() { RelativeContainer() { Text(this.message) .id('Index') .fontSize(50) .fontWeight(FontWeight.Bold) .alignRules({ center: { anchor: '__container__', align: VerticalAlign.Center }, middle: { anchor: '__container__', align: HorizontalAlign.Center } }) .onClick(() => { let contentNode = new ComponentContent(this.uiContext, wrapBuilder(buildCreativeLoadingDialog), () => { }); this.uiContext.getPromptAction().openCustomDialog(contentNode); }) } .height('100%') .width('100%') } } ``` - 被@Reusable装饰的自定义组件在复用时,会递归调用该自定义组件及其所有子组件的aboutToReuse回调函数。若在子组件的aboutToReuse函数中修改了父组件的状态变量,此次修改将不会生效,请避免此类用法。若需设置父组件的状态变量,可使用setTimeout设置延迟执行,将任务抛出组件复用的作用范围,使修改生效。 【反例】 在子组件的aboutToReuse中,直接修改父组件的状态变量。 ```ts class BasicDataSource implements IDataSource { private listener: DataChangeListener | undefined = undefined; public dataArray: number[] = []; totalCount(): number { return this.dataArray.length; } getData(index: number): number { return this.dataArray[index]; } registerDataChangeListener(listener: DataChangeListener): void { this.listener = listener; } unregisterDataChangeListener(listener: DataChangeListener): void { this.listener = undefined; } } @Entry @Component struct Index { private data: BasicDataSource = new BasicDataSource(); aboutToAppear(): void { for (let index = 1; index < 20; index++) { this.data.dataArray.push(index); } } build() { List() { LazyForEach(this.data, (item: number, index: number) => { ListItem() { ReuseComponent({ num: item }) } }, (item: number, index: number) => index.toString()) }.cachedCount(0) } } @Reusable @Component struct ReuseComponent { @State num: number = 0; aboutToReuse(params: ESObject): void { this.num = params.num; } build() { Column() { Text('ReuseComponent num:' + this.num.toString()) ReuseComponentChild({ num: this.num }) Button('plus') .onClick(() => { this.num += 10; }) } .height(200) } } @Component struct ReuseComponentChild { @Link num: number; aboutToReuse(params: ESObject): void { this.num = -1 * params.num; } build() { Text('ReuseComponentChild num:' + this.num.toString()) } } ``` 【正例】 在子组件的aboutToReuse中,使用setTimeout,将修改抛出组件复用的作用范围。 ```ts class BasicDataSource implements IDataSource { private listener: DataChangeListener | undefined = undefined; public dataArray: number[] = []; totalCount(): number { return this.dataArray.length; } getData(index: number): number { return this.dataArray[index]; } registerDataChangeListener(listener: DataChangeListener): void { this.listener = listener; } unregisterDataChangeListener(listener: DataChangeListener): void { this.listener = undefined; } } @Entry @Component struct Index { private data: BasicDataSource = new BasicDataSource(); aboutToAppear(): void { for (let index = 1; index < 20; index++) { this.data.dataArray.push(index); } } build() { List() { LazyForEach(this.data, (item: number, index: number) => { ListItem() { ReuseComponent({ num: item }) } }, (item: number, index: number) => index.toString()) }.cachedCount(0) } } @Reusable @Component struct ReuseComponent { @State num: number = 0; aboutToReuse(params: ESObject): void { this.num = params.num; } build() { Column() { Text('ReuseComponent num:' + this.num.toString()) ReuseComponentChild({ num: this.num }) Button('plus') .onClick(() => { this.num += 10; }) } .height(200) } } @Component struct ReuseComponentChild { @Link num: number; aboutToReuse(params: ESObject): void { setTimeout(() => { this.num = -1 * params.num; }, 1) } build() { Text('ReuseComponentChild num:' + this.num.toString()) } } ``` - ComponentContent不支持传入\@Reusable装饰器装饰的自定义组件。 ```ts import { ComponentContent } from "@kit.ArkUI"; @Builder function buildCreativeLoadingDialog(closedClick: () => void) { Crash() } // 如果注释掉就可以正常弹出弹窗,如果加上@Reusable就直接crash。 @Reusable @Component export struct Crash { build() { Column() { Text("Crash") .fontSize(12) .lineHeight(18) .fontColor(Color.Blue) .margin({ left: 6 }) }.width('100%') .height('100%') .justifyContent(FlexAlign.Center) } } @Entry @Component struct Index { @State message: string = 'Hello World'; private uiContext = this.getUIContext(); build() { RelativeContainer() { Text(this.message) .id('Index') .fontSize(50) .fontWeight(FontWeight.Bold) .alignRules({ center: { anchor: '__container__', align: VerticalAlign.Center }, middle: { anchor: '__container__', align: HorizontalAlign.Center } }) .onClick(() => { // ComponentContent底层是BuilderNode,BuilderNode不支持传入@Reusable注解的自定义组件。 let contentNode = new ComponentContent(this.uiContext, wrapBuilder(buildCreativeLoadingDialog), () => { }); this.uiContext.getPromptAction().openCustomDialog(contentNode); }) } .height('100%') .width('100%') } } ``` - \@Reusable装饰器不建议嵌套使用,会增加内存,降低复用效率,加大维护难度。嵌套使用会导致额外缓存池的生成,各缓存池拥有相同树状结构,复用效率低下。此外,嵌套使用会使生命周期管理复杂,资源和变量共享困难。 ## 使用场景 ### 动态布局更新 重复创建与移除视图可能引起频繁的布局计算,从而影响帧率。采用组件复用可以避免不必要的视图创建与布局计算,提升性能。 以下示例中,将Child自定义组件标记为复用组件,通过Button点击更新Child,触发复用。 ```ts // xxx.ets export class Message { value: string | undefined; constructor(value: string) { this.value = value; } } @Entry @Component struct Index { @State switch: boolean = true; build() { Column() { Button('Hello') .fontSize(30) .fontWeight(FontWeight.Bold) .onClick(() => { this.switch = !this.switch; }) if (this.switch) { // 如果只有一个复用的组件,可以不用设置reuseId。 Child({ message: new Message('Child') }) .reuseId('Child') } } .height("100%") .width('100%') } } @Reusable @Component struct Child { @State message: Message = new Message('AboutToReuse'); aboutToReuse(params: Record) { console.info("Recycle====Child=="); this.message = params.message as Message; } build() { Column() { Text(this.message.value) .fontSize(30) } .borderWidth(1) .height(100) } } ``` ### 列表滚动配合LazyForEach使用 - 当应用展示大量数据的列表并进行滚动操作时,频繁创建和销毁列表项视图可能导致卡顿和性能问题。使用列表组件的组件复用机制可以重用已创建的列表项视图,提高滚动流畅度。 - 以下示例代码将CardView自定义组件标记为复用组件,List上下滑动,触发CardView复用。 ```ts class MyDataSource implements IDataSource { private dataArray: string[] = []; private listener: DataChangeListener | undefined; public totalCount(): number { return this.dataArray.length; } public getData(index: number): string { return this.dataArray[index]; } public pushData(data: string): void { this.dataArray.push(data); } public reloadListener(): void { this.listener?.onDataReloaded(); } public registerDataChangeListener(listener: DataChangeListener): void { this.listener = listener; } public unregisterDataChangeListener(listener: DataChangeListener): void { this.listener = undefined; } } @Entry @Component struct ReuseDemo { private data: MyDataSource = new MyDataSource(); aboutToAppear() { for (let i = 1; i < 1000; i++) { this.data.pushData(i + ""); } } // ... build() { Column() { List() { LazyForEach(this.data, (item: string) => { ListItem() { CardView({ item: item }) } }, (item: string) => item) } } } } // 复用组件 @Reusable @Component export struct CardView { // 被\@State修饰的变量item才能更新,未被\@State修饰的变量不会更新。 @State item: string = ''; aboutToReuse(params: Record): void { this.item = params.item as string; } build() { Column() { Text(this.item) .fontSize(30) } .borderWidth(1) .height(100) } } ``` ### 列表滚动-if使用场景 以下示例代码将OneMoment自定义组件标记为复用组件。当List上下滑动时,会触发OneMoment的复用。设置reuseId可为复用组件分配复用组,相同reuseId的组件将在同一复用组中复用。单个复用组件无需设置reuseId。使用reuseId标识复用组件,可避免重复执行if语句的删除和重新创建逻辑,提高复用效率和性能。 ```ts @Entry @Component struct Index { private dataSource = new MyDataSource(); aboutToAppear(): void { for (let i = 0; i < 20; i++) { let title = i + 1 + "test_if"; this.dataSource.pushData(new FriendMoment(i.toString(), title, 'app.media.app_icon')); } for (let i = 0; i < 50; i++) { let title = i + 1 + "test_if"; this.dataSource.pushData(new FriendMoment(i.toString(), title, '')); } } build() { Column() { // TopBar() List({ space: 3 }) { LazyForEach(this.dataSource, (moment: FriendMoment) => { ListItem() { // 使用reuseId进行组件复用的控制。 OneMoment({ moment: moment }) .reuseId((moment.image !== '') ? 'withImage' : 'noImage') } }, (moment: FriendMoment) => moment.id) } .cachedCount(0) } } } class FriendMoment { id: string = ''; text: string = ''; title: string = ''; image: string = ''; answers: Array = []; constructor(id: string, title: string, image: string) { this.text = id; this.title = title; this.image = image; } } @Reusable @Component export struct OneMoment { @Prop moment: FriendMoment; // 复用id相同的组件才能触发复用。 aboutToReuse(params: ESObject): void { console.log("=====aboutToReuse====OneMoment==复用了==" + this.moment.text); } build() { Column() { Text(this.moment.text) // if分支判断。 if (this.moment.image !== '') { Flex({ wrap: FlexWrap.Wrap }) { Image($r(this.moment.image)).height(50).width(50) Image($r(this.moment.image)).height(50).width(50) Image($r(this.moment.image)).height(50).width(50) Image($r(this.moment.image)).height(50).width(50) } } } } } class BasicDataSource implements IDataSource { private listeners: DataChangeListener[] = []; private originDataArray: T[] = []; public totalCount(): number { return 0; } public getData(index: number): T { return this.originDataArray[index]; } registerDataChangeListener(listener: DataChangeListener): void { if (this.listeners.indexOf(listener) < 0) { this.listeners.push(listener); } } unregisterDataChangeListener(listener: DataChangeListener): void { const pos = this.listeners.indexOf(listener); if (pos >= 0) { this.listeners.splice(pos, 1); } } notifyDataAdd(index: number): void { this.listeners.forEach(listener => { listener.onDataAdd(index); }); } } export class MyDataSource extends BasicDataSource { private dataArray: T[] = []; public totalCount(): number { return this.dataArray.length; } public getData(index: number): T { return this.dataArray[index]; } public pushData(data: T): void { this.dataArray.push(data); this.notifyDataAdd(this.dataArray.length - 1); } } ``` ### 列表滚动-Foreach使用场景 使用Foreach创建可复用的自定义组件,由于Foreach渲染控制语法的全展开属性,导致复用组件无法复用。示例中点击update,数据刷新成功,但滑动列表时,ListItemView无法复用。点击clear,再次点击update,ListItemView复用成功,因为一帧内重复创建多个已被销毁的自定义组件。 ```ts // xxx.ets class MyDataSource implements IDataSource { private dataArray: string[] = []; public totalCount(): number { return this.dataArray.length; } public getData(index: number): string { return this.dataArray[index]; } public pushData(data: string): void { this.dataArray.push(data); } public registerDataChangeListener(listener: DataChangeListener): void { } public unregisterDataChangeListener(listener: DataChangeListener): void { } } @Entry @Component struct Index { private data: MyDataSource = new MyDataSource(); private data02: MyDataSource = new MyDataSource(); @State isShow: boolean = true; @State dataSource: ListItemObject[] = []; aboutToAppear() { for (let i = 0; i < 100; i++) { this.data.pushData(i.toString()); } for (let i = 30; i < 80; i++) { this.data02.pushData(i.toString()); } } build() { Column() { Row() { Button('clear').onClick(() => { for (let i = 1; i < 50; i++) { this.dataSource.pop(); } }).height(40) Button('update').onClick(() => { for (let i = 1; i < 50; i++) { let obj = new ListItemObject(); obj.id = i; obj.uuid = Math.random().toString(); obj.isExpand = false; this.dataSource.push(obj); } }).height(40) } List({ space: 10 }) { ForEach(this.dataSource, (item: ListItemObject) => { ListItem() { ListItemView({ obj: item }) } }, (item: ListItemObject) => { return item.uuid.toString(); }) }.cachedCount(0) .width('100%') .height('100%') } } } @Reusable @Component struct ListItemView { @ObjectLink obj: ListItemObject; @State item: string = ''; aboutToAppear(): void { // 点击 update,首次进入,上下滑动,由于Foreach折叠展开属性,无法复用。 console.log("=====aboutToAppear=====ListItemView==创建了==" + this.item); } aboutToReuse(params: ESObject) { this.item = params.item; // 点击clear,再次update,复用成功。 // 符合一帧内重复创建多个已被销毁的自定义组件。 console.log("=====aboutToReuse====ListItemView==复用了==" + this.item); } build() { Column({ space: 10 }) { Text(`${this.obj.id}.标题`) .fontSize(16) .fontColor('#000000') .padding({ top: 20, bottom: 20, }) if (this.obj.isExpand) { Text('') .fontSize(14) .fontColor('#999999') } } .width('100%') .borderRadius(10) .backgroundColor(Color.White) .padding(15) .onClick(() => { this.obj.isExpand = !this.obj.isExpand; }) } } @Observed class ListItemObject { uuid: string = ""; id: number = 0; isExpand: boolean = false; } ``` ### Grid使用场景 示例中使用\@Reusable装饰器修饰GridItem中的自定义组件ReusableChildComponent,即表示其具备组件复用的能力。 使用aboutToReuse可以在 Grid 滑动时,从复用缓存中加入到组件树之前触发,从而更新组件状态变量,展示正确内容。 需要注意的是无需在aboutToReuse中对[\@Link](arkts-link.md)、[\@StorageLink](arkts-appstorage.md#storagelink)、[\@ObjectLink](arkts-observed-and-objectlink.md)、[\@Consume](arkts-provide-and-consume.md)等自动更新值的状态变量进行更新,可能触发不必要的组件刷新。 ```ts // MyDataSource类实现IDataSource接口。 class MyDataSource implements IDataSource { private dataArray: number[] = []; public pushData(data: number): void { this.dataArray.push(data); } // 数据源的数据总量。 public totalCount(): number { return this.dataArray.length; } // 返回指定索引位置的数据。 public getData(index: number): number { return this.dataArray[index]; } registerDataChangeListener(listener: DataChangeListener): void { } unregisterDataChangeListener(listener: DataChangeListener): void { } } @Entry @Component struct MyComponent { // 数据源。 private data: MyDataSource = new MyDataSource(); aboutToAppear() { for (let i = 1; i < 1000; i++) { this.data.pushData(i); } } build() { Column({ space: 5 }) { Grid() { LazyForEach(this.data, (item: number) => { GridItem() { // 使用可复用自定义组件。 ReusableChildComponent({ item: item }) } }, (item: string) => item) } .cachedCount(2) // 设置GridItem的缓存数量。 .columnsTemplate('1fr 1fr 1fr') .columnsGap(10) .rowsGap(10) .margin(10) .height(500) .backgroundColor(0xFAEEE0) } } } @Reusable @Component struct ReusableChildComponent { @State item: number = 0; // aboutToReuse从复用缓存中加入到组件树之前调用,可在此处更新组件的状态变量以展示正确的内容。 // aboutToReuse参数类型已不支持any,这里使用Record指定明确的数据类型。Record用于构造一个对象类型,其属性键为Keys,属性值为Type。 aboutToReuse(params: Record) { this.item = params.item; } build() { Column() { // 请开发者自行在src/main/resources/base/media路径下添加app.media.app_icon图片,否则运行时会因资源缺失而报错。 Image($r('app.media.app_icon')) .objectFit(ImageFit.Fill) .layoutWeight(1) Text(`图片${this.item}`) .fontSize(16) .textAlign(TextAlign.Center) } .width('100%') .height(120) .backgroundColor(0xF9CF93) } } ``` ### WaterFlow使用场景 - 在WaterFlow滑动场景中,FlowItem及其子组件频繁创建和销毁。可以将FlowItem中的组件封装成自定义组件,并使用\@Reusable装饰器修饰,实现组件复用。 ```ts class WaterFlowDataSource implements IDataSource { private dataArray: number[] = []; private listeners: DataChangeListener[] = []; constructor() { for (let i = 0; i <= 60; i++) { this.dataArray.push(i); } } // 获取索引对应的数据。 public getData(index: number): number { return this.dataArray[index]; } // 通知控制器增加数据。 notifyDataAdd(index: number): void { this.listeners.forEach(listener => { listener.onDataAdd(index); }); } // 获取数据总数。 public totalCount(): number { return this.dataArray.length; } // 注册改变数据的控制器。 registerDataChangeListener(listener: DataChangeListener): void { if (this.listeners.indexOf(listener) < 0) { this.listeners.push(listener); } } // 注销改变数据的控制器。 unregisterDataChangeListener(listener: DataChangeListener): void { const pos = this.listeners.indexOf(listener); if (pos >= 0) { this.listeners.splice(pos, 1); } } // 在数据尾部增加一个元素。 public addLastItem(): void { this.dataArray.splice(this.dataArray.length, 0, this.dataArray.length); this.notifyDataAdd(this.dataArray.length - 1); } } @Reusable @Component struct ReusableFlowItem { @State item: number = 0; // 从复用缓存中加入到组件树之前调用,可在此处更新组件的状态变量以展示正确的内容。 aboutToReuse(params: ESObject) { this.item = params.item; console.log("=====aboutToReuse====FlowItem==复用了==" + this.item); } aboutToRecycle(): void { console.log("=====aboutToRecycle====FlowItem==回收了==" + this.item); } build() { // 请开发者自行在src/main/resources/base/media路径下添加app.media.app_icon图片,否则运行时会因资源缺失而报错。 Column() { Text("N" + this.item).fontSize(24).height('26').margin(10) Image($r('app.media.app_icon')) .objectFit(ImageFit.Cover) .width(50) .height(50) } } } @Entry @Component struct Index { @State minSize: number = 50; @State maxSize: number = 80; @State fontSize: number = 24; @State colors: number[] = [0xFFC0CB, 0xDA70D6, 0x6B8E23, 0x6A5ACD, 0x00FFFF, 0x00FF7F]; scroller: Scroller = new Scroller(); dataSource: WaterFlowDataSource = new WaterFlowDataSource(); private itemWidthArray: number[] = []; private itemHeightArray: number[] = []; // 计算flow item宽/高。 getSize() { let ret = Math.floor(Math.random() * this.maxSize); return (ret > this.minSize ? ret : this.minSize); } // 保存flow item宽/高。 getItemSizeArray() { for (let i = 0; i < 100; i++) { this.itemWidthArray.push(this.getSize()); this.itemHeightArray.push(this.getSize()); } } aboutToAppear() { this.getItemSizeArray(); } build() { Stack({ alignContent: Alignment.TopStart }) { Column({ space: 2 }) { Button('back top') .height('5%') .onClick(() => { // 点击后回到顶部。 this.scroller.scrollEdge(Edge.Top); }) WaterFlow({ scroller: this.scroller }) { LazyForEach(this.dataSource, (item: number) => { FlowItem() { ReusableFlowItem({ item: item }) }.onAppear(() => { if (item + 20 == this.dataSource.totalCount()) { for (let i = 0; i < 50; i++) { this.dataSource.addLastItem(); } } }) }) } } } } } ``` ### Swiper使用场景 - 在Swiper滑动场景中,条目中的子组件频繁创建和销毁。可以将这些子组件封装成自定义组件,并使用\@Reusable装饰器修饰,以实现组件复用。 ```ts @Entry @Component struct Index { private dataSource = new MyDataSource(); aboutToAppear(): void { for (let i = 0; i < 1000; i++) { let title = i + 1 + "test_swiper"; let answers = ["test1", "test2", "test3", "test4"]; // 请开发者自行在src/main/resources/base/media路径下添加app.media.app_icon图片,否则运行时会因资源缺失而报错。 this.dataSource.pushData(new Question(i.toString(), title, $r('app.media.app_icon'), answers)); } } build() { Column({ space: 5 }) { Swiper() { LazyForEach(this.dataSource, (item: Question) => { QuestionSwiperItem({ itemData: item }) }, (item: Question) => item.id) } } .width('100%') .margin({ top: 5 }) } } class Question { id: string = ''; title: ResourceStr = ''; image: ResourceStr = ''; answers: Array = []; constructor(id: string, title: ResourceStr, image: ResourceStr, answers: Array) { this.id = id; this.title = title; this.image = image; this.answers = answers; } } @Reusable @Component struct QuestionSwiperItem { @State itemData: Question | null = null; aboutToReuse(params: Record): void { this.itemData = params.itemData as Question; console.info("===aboutToReuse====QuestionSwiperItem=="); } build() { Column() { Text(this.itemData?.title) .fontSize(18) .fontColor($r('sys.color.ohos_id_color_primary')) .alignSelf(ItemAlign.Start) .margin({ top: 10, bottom: 16 }) Image(this.itemData?.image) .width('100%') .borderRadius(12) .objectFit(ImageFit.Contain) .margin({ bottom: 16 }) .height(80) .width(80) Column({ space: 16 }) { ForEach(this.itemData?.answers, (item: Resource) => { Text(item) .fontSize(16) .fontColor($r('sys.color.ohos_id_color_primary')) }, (item: ResourceStr) => JSON.stringify(item)) } .width('100%') .alignItems(HorizontalAlign.Start) } .width('100%') .padding({ left: 16, right: 16 }) } } class BasicDataSource implements IDataSource { private listeners: DataChangeListener[] = []; private originDataArray: T[] = []; public totalCount(): number { return 0; } public getData(index: number): T { return this.originDataArray[index]; } registerDataChangeListener(listener: DataChangeListener): void { if (this.listeners.indexOf(listener) < 0) { this.listeners.push(listener); } } unregisterDataChangeListener(listener: DataChangeListener): void { const pos = this.listeners.indexOf(listener); if (pos >= 0) { this.listeners.splice(pos, 1); } } notifyDataAdd(index: number): void { this.listeners.forEach(listener => { listener.onDataAdd(index); }); } } export class MyDataSource extends BasicDataSource { private dataArray: T[] = []; public totalCount(): number { return this.dataArray.length; } public getData(index: number): T { return this.dataArray[index]; } public pushData(data: T): void { this.dataArray.push(data); this.notifyDataAdd(this.dataArray.length - 1); } } ``` ### 列表滚动-ListItemGroup使用场景 - 可以视作特殊List滑动场景,将ListItem需要移除重建的子组件封装成自定义组件,并使用\@Reusable装饰器修饰,使其具备组件复用能力。 ```ts @Entry @Component struct ListItemGroupAndReusable { data: DataSrc2 = new DataSrc2(); @Builder itemHead(text: string) { Text(text) .fontSize(20) .backgroundColor(0xAABBCC) .width('100%') .padding(10) } aboutToAppear() { for (let i = 0; i < 10000; i++) { let data_1 = new DataSrc1(); for (let j = 0; j < 12; j++) { data_1.Data.push(`测试条目数据: ${i} - ${j}`); } this.data.Data.push(data_1); } } build() { Stack() { List() { LazyForEach(this.data, (item: DataSrc1, index: number) => { ListItemGroup({ header: this.itemHead(index.toString()) }) { LazyForEach(item, (ii: string, index: number) => { ListItem() { Inner({ str: ii }) } }) } .width('100%') .height('60vp') }) } } .width('100%') .height('100%') } } @Reusable @Component struct Inner { @State str: string = ''; aboutToReuse(param: ESObject) { this.str = param.str; } build() { Text(this.str) } } class DataSrc1 implements IDataSource { listeners: DataChangeListener[] = []; Data: string[] = []; public totalCount(): number { return this.Data.length; } public getData(index: number): string { return this.Data[index]; } // 该方法为框架侧调用,为LazyForEach组件向其数据源处添加listener监听。 registerDataChangeListener(listener: DataChangeListener): void { if (this.listeners.indexOf(listener) < 0) { this.listeners.push(listener); } } // 该方法为框架侧调用,为对应的LazyForEach组件在数据源处去除listener监听。 unregisterDataChangeListener(listener: DataChangeListener): void { const pos = this.listeners.indexOf(listener); if (pos >= 0) { this.listeners.splice(pos, 1); } } // 通知LazyForEach组件需要重载所有子组件。 notifyDataReload(): void { this.listeners.forEach(listener => { listener.onDataReloaded(); }); } // 通知LazyForEach组件需要在index对应索引处添加子组件。 notifyDataAdd(index: number): void { this.listeners.forEach(listener => { listener.onDataAdd(index); }); } // 通知LazyForEach组件在index对应索引处数据有变化,需要重建该子组件。 notifyDataChange(index: number): void { this.listeners.forEach(listener => { listener.onDataChange(index); }); } // 通知LazyForEach组件需要在index对应索引处删除该子组件。 notifyDataDelete(index: number): void { this.listeners.forEach(listener => { listener.onDataDelete(index); }); } // 通知LazyForEach组件将from索引和to索引处的子组件进行交换。 notifyDataMove(from: number, to: number): void { this.listeners.forEach(listener => { listener.onDataMove(from, to); }); } } class DataSrc2 implements IDataSource { listeners: DataChangeListener[] = []; Data: DataSrc1[] = []; public totalCount(): number { return this.Data.length; } public getData(index: number): DataSrc1 { return this.Data[index]; } // 该方法为框架侧调用,为LazyForEach组件向其数据源处添加listener监听。 registerDataChangeListener(listener: DataChangeListener): void { if (this.listeners.indexOf(listener) < 0) { this.listeners.push(listener); } } // 该方法为框架侧调用,为对应的LazyForEach组件在数据源处去除listener监听。 unregisterDataChangeListener(listener: DataChangeListener): void { const pos = this.listeners.indexOf(listener); if (pos >= 0) { this.listeners.splice(pos, 1); } } // 通知LazyForEach组件需要重载所有子组件。 notifyDataReload(): void { this.listeners.forEach(listener => { listener.onDataReloaded(); }); } // 通知LazyForEach组件需要在index对应索引处添加子组件。 notifyDataAdd(index: number): void { this.listeners.forEach(listener => { listener.onDataAdd(index); }); } // 通知LazyForEach组件在index对应索引处数据有变化,需要重建该子组件。 notifyDataChange(index: number): void { this.listeners.forEach(listener => { listener.onDataChange(index); }); } // 通知LazyForEach组件需要在index对应索引处删除该子组件。 notifyDataDelete(index: number): void { this.listeners.forEach(listener => { listener.onDataDelete(index); }); } // 通知LazyForEach组件将from索引和to索引处的子组件进行交换。 notifyDataMove(from: number, to: number): void { this.listeners.forEach(listener => { listener.onDataMove(from, to); }); } } ``` ### 多种条目类型使用场景 **标准型** 复用组件的布局相同,示例参见本文列表滚动部分的描述。 **有限变化型** 复用组件间存在差异,但类型有限。例如,可以通过显式设置两个reuseId或使用两个自定义组件来实现复用。 ```ts class MyDataSource implements IDataSource { private dataArray: string[] = []; private listener: DataChangeListener | undefined; public totalCount(): number { return this.dataArray.length; } public getData(index: number): string { return this.dataArray[index]; } public pushData(data: string): void { this.dataArray.push(data); } public reloadListener(): void { this.listener?.onDataReloaded(); } public registerDataChangeListener(listener: DataChangeListener): void { this.listener = listener; } public unregisterDataChangeListener(listener: DataChangeListener): void { this.listener = undefined; } } @Entry @Component struct Index { private data: MyDataSource = new MyDataSource(); aboutToAppear() { for (let i = 0; i < 1000; i++) { this.data.pushData(i + ""); } } build() { Column() { List({ space: 10 }) { LazyForEach(this.data, (item: number) => { ListItem() { ReusableComponent({ item: item }) // 设置两种有限变化的reuseId .reuseId(item % 2 === 0 ? 'ReusableComponentOne' : 'ReusableComponentTwo') } .backgroundColor(Color.Orange) .width('100%') }, (item: number) => item.toString()) } .cachedCount(2) } } } @Reusable @Component struct ReusableComponent { @State item: number = 0; aboutToReuse(params: ESObject) { this.item = params.item; } build() { Column() { // 组件内部根据类型差异渲染 if (this.item % 2 === 0) { Text(`Item ${this.item} ReusableComponentOne`) .fontSize(20) .margin({ left: 10 }) } else { Text(`Item ${this.item} ReusableComponentTwo`) .fontSize(20) .margin({ left: 10 }) } }.margin({ left: 10, right: 10 }) } } ``` **组合型** 复用组件间存在多种差异,但通常具备共同的子组件。将三种复用组件以组合型方式转换为Builder函数后,内部的共享子组件将统一置于父组件MyComponent之下。复用这些子组件时,缓存池在父组件层面实现共享,减少组件创建过程中的资源消耗。 ```ts class MyDataSource implements IDataSource { private dataArray: string[] = []; private listener: DataChangeListener | undefined; public totalCount(): number { return this.dataArray.length; } public getData(index: number): string { return this.dataArray[index]; } public pushData(data: string): void { this.dataArray.push(data); } public reloadListener(): void { this.listener?.onDataReloaded(); } public registerDataChangeListener(listener: DataChangeListener): void { this.listener = listener; } public unregisterDataChangeListener(listener: DataChangeListener): void { this.listener = undefined; } } @Entry @Component struct MyComponent { private data: MyDataSource = new MyDataSource(); aboutToAppear() { for (let i = 0; i < 1000; i++) { this.data.pushData(i.toString()); } } // itemBuilderOne作为复用组件的写法未展示,以下为转为Builder之后的写法。 @Builder itemBuilderOne(item: string) { Column() { ChildComponentA({ item: item }) ChildComponentB({ item: item }) ChildComponentC({ item: item }) } } // itemBuilderTwo转为Builder之后的写法。 @Builder itemBuilderTwo(item: string) { Column() { ChildComponentA({ item: item }) ChildComponentC({ item: item }) ChildComponentD({ item: item }) } } // itemBuilderThree转为Builder之后的写法。 @Builder itemBuilderThree(item: string) { Column() { ChildComponentA({ item: item }) ChildComponentB({ item: item }) ChildComponentD({ item: item }) } } build() { List({ space: 40 }) { LazyForEach(this.data, (item: string, index: number) => { ListItem() { if (index % 3 === 0) { this.itemBuilderOne(item) } else if (index % 5 === 0) { this.itemBuilderTwo(item) } else { this.itemBuilderThree(item) } } .backgroundColor('#cccccc') .width('100%') .onAppear(() => { console.log(`ListItem ${index} onAppear`); }) }, (item: number) => item.toString()) } .width('100%') .height('100%') .cachedCount(0) } } @Reusable @Component struct ChildComponentA { @State item: string = ''; aboutToReuse(params: ESObject) { console.log(`ChildComponentA ${params.item} Reuse ${this.item}`); this.item = params.item; } aboutToRecycle(): void { console.log(`ChildComponentA ${this.item} Recycle`); } build() { Column() { Text(`Item ${this.item} Child Component A`) .fontSize(20) .margin({ left: 10 }) .fontColor(Color.Blue) Grid() { ForEach((new Array(20)).fill(''), (item: string, index: number) => { GridItem() { // 请开发者自行在src/main/resources/base/media路径下添加app.media.startIcon图片,否则运行时会因资源缺失而报错。 Image($r('app.media.startIcon')) .height(20) } }) } .columnsTemplate('1fr 1fr 1fr 1fr 1fr') .rowsTemplate('1fr 1fr 1fr 1fr') .columnsGap(10) .width('90%') .height(160) } .margin({ left: 10, right: 10 }) .backgroundColor(0xFAEEE0) } } @Reusable @Component struct ChildComponentB { @State item: string = ''; aboutToReuse(params: ESObject) { this.item = params.item; } build() { Row() { Text(`Item ${this.item} Child Component B`) .fontSize(20) .margin({ left: 10 }) .fontColor(Color.Red) }.margin({ left: 10, right: 10 }) } } @Reusable @Component struct ChildComponentC { @State item: string = ''; aboutToReuse(params: ESObject) { this.item = params.item; } build() { Row() { Text(`Item ${this.item} Child Component C`) .fontSize(20) .margin({ left: 10 }) .fontColor(Color.Green) }.margin({ left: 10, right: 10 }) } } @Reusable @Component struct ChildComponentD { @State item: string = ''; aboutToReuse(params: ESObject) { this.item = params.item; } build() { Row() { Text(`Item ${this.item} Child Component D`) .fontSize(20) .margin({ left: 10 }) .fontColor(Color.Orange) }.margin({ left: 10, right: 10 }) } } ```