@Reusable装饰器:V1组件复用
@Reusable装饰的自定义组件支持组件复用。当自定义组件从组件树上移除时,会被存入缓存池,后续在创建相同类型的组件节点时,将优先复用缓存池中的组件对象,从而避免重复创建和销毁,提升性能。
说明:
API version 10开始支持@Reusable,支持在ArkTS中使用。
关于组件复用的原理与使用、优化方法、适用场景,请参考组件复用最佳实践。
概述
@Reusable用于装饰自定义组件,表示该自定义组件具有被复用的能力。
在开发复杂界面时,UI渲染效率是一个需要考虑的问题。例如在长列表快速滑动时,大量列表项的创建和销毁可能导致界面卡顿。组件复用是一种优化UI性能的重要方法。通过复用先前创建并且已经下树的组件对象,降低组件创建和销毁的频率,从而减小计算开销,提升UI渲染效率。
注意:
- @Reusable装饰的自定义组件在从组件树中移除时,自定义组件(包含视图节点、组件实例和状态上下文)将被放入其父自定义组件的缓存池中。后续创建新自定义组件节点时,将优先复用缓存池中的节点,从而节约组件重新创建的时间。
- @Reusable提供了aboutToRecycle和aboutToReuse两个生命周期,在组件被回收时调用aboutToRecycle,在组件被复用时调用aboutToReuse。开发者可以在这两个生命周期中实现组件回收、复用相关的业务逻辑。
- @Reusable装饰的自定义组件下有子组件时,会在回收和复用时递归调用子组件的aboutToRecycle和aboutToReuse(与子组件是否被@Reusable标记无关),直到遍历完所有子组件。
- 组件复用前后应保持组件结构不变。针对组件结构存在差异的场景,可以使用reuseId来区分不同结构的复用组件。
- 当@Reusable装饰的自定义组件无子组件时,不会触发回收和复用。
限制条件
仅用于自定义组件
@Reusable装饰器仅用于自定义组件@Component,不可与@Builder搭配使用。
@Reusable不支持跟@ComponentV2搭配使用,@ComponentV2组件复用推荐@ReusableV2装饰器。
import { ComponentContent } from '@kit.ArkUI';
// @Builder不能与@Reusable搭配使用。
// @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中,直接修改父组件的状态变量。
class IncorrectBasicDataSource 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 IncorrectIndex {
private data: IncorrectBasicDataSource = new IncorrectBasicDataSource();
aboutToAppear(): void {
for (let index = 1; index < 20; index++) {
this.data.dataArray.push(index);
}
}
build() {
List() {
LazyForEach(this.data, (item: number, index: number) => {
ListItem() {
IncorrectReuseComponent({ num: item });
}
}, (item: number, index: number) => index.toString())
}.cachedCount(0)
}
}
@Reusable
@Component
struct IncorrectReuseComponent {
@State num: number = 0;
aboutToReuse(params: ESObject): void {
this.num = params.num;
}
build() {
Column() {
Text('ReuseComponent num:' + this.num.toString())
IncorrectReuseComponentChild({ num: this.num })
Button('plus')
.onClick(() => {
this.num += 10;
})
}
.height(200)
}
}
@Component
struct IncorrectReuseComponentChild {
@Link num: number;
aboutToReuse(params: ESObject): void {
this.num = -1 * params.num;
}
build() {
Text('ReuseComponentChild num:' + this.num.toString())
}
}
【正例】
在子组件的aboutToReuse中,使用setTimeout,将修改移出组件复用的作用范围。
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++) { // 循环20次
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; // 每次点击增加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());
}
}
组件结构需一致
被@Reusable装饰的自定义组件在复用前后,应保持组件的结构不变。否则,会在复用过程中创建或销毁子组件,降低复用效率和性能,甚至造成应用行为异常。
对于复用过程中创建的子组件,框架会在其创建后依次调用aboutToReuse方法和aboutToAppear方法。在调用aboutToReuse方法时,由于其aboutToAppear方法还未执行,且内部子组件还未创建,因此aboutToReuse方法中依赖aboutToAppear方法执行结果,或依赖内部子组件状态的相关操作会引起预期外的行为。在调用aboutToReuse方法后,框架会再调用aboutToAppear方法并初始化组件。
针对组件结构存在差异的场景,开发者需要通过设定不同的reuseId来进行区分,具体方式请参考多种条目类型使用场景。
【反例】
组件结构存在差异,但未通过reuseId进行区分。
以下示例中,先点击“show/hide branch A”按钮,组件被回收,再点击“show/hide branch B”按钮,组件被复用。子组件ReusableChildB在复用过程中被创建,aboutToReuse方法和aboutToAppear方法被依次调用。
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG = '[Sample_ReusableComponent]';
const DOMAIN = 0xF811;
const BUNDLE = 'ReusableComponent_';
@Entry
@Component
struct Index {
@State showBranchA: boolean = true;
@State showBranchB: boolean = false;
build() {
Column({ space: 5 }) {
Button('show/hide branch A')
.onClick(() => {
this.showBranchA = !this.showBranchA;
})
if (this.showBranchA) {
ReusableComponent({ flag: true })
}
Button('show/hide branch B')
.onClick(() => {
this.showBranchB = !this.showBranchB;
})
if (this.showBranchB) {
ReusableComponent({ flag: false })
}
}
}
}
@Reusable
@Component
struct ReusableComponent {
@Require @Prop flag: boolean = true;
aboutToAppear() {
hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableComponent aboutToAppear');
}
aboutToReuse(params: ESObject) {
hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableComponent aboutToReuse');
this.flag = params.flag;
}
build() {
Column({ space: 5 }) {
Text('ReusableComponent')
if (this.flag) {
ReusableChildA()
} else {
ReusableChildB()
}
}.border({ width: 1 })
}
}
@Component
struct ReusableChildA {
aboutToAppear() {
hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableChildA aboutToAppear');
}
aboutToReuse() {
hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableChildA aboutToReuse');
}
build() {
Text('ReusableChildA')
.border({ width: 1 })
}
}
@Component
struct ReusableChildB {
aboutToAppear() {
hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableChildB aboutToAppear');
}
aboutToReuse() {
hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableChildB aboutToReuse');
}
build() {
Text('ReusableChildB')
.border({ width: 1 })
}
}
【正例】
组件结构存在差异,通过reuseId进行区分。
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG = '[Sample_ReusableComponent]';
const DOMAIN = 0xF811;
const BUNDLE = 'ReusableComponent_';
@Entry
@Component
struct Index {
@State showBranchA: boolean = true;
@State showBranchB: boolean = false;
build() {
Column({ space: 5 }) {
Button('show/hide branch A')
.onClick(() => {
this.showBranchA = !this.showBranchA;
})
if (this.showBranchA) {
ReusableComponent({ flag: true })
.reuseId('ReuseA') // 通过reuseId区分不同结构的复用组件
}
Button('show/hide branch B')
.onClick(() => {
this.showBranchB = !this.showBranchB;
})
if (this.showBranchB) {
ReusableComponent({ flag: false })
.reuseId('ReuseB') // 通过reuseId区分不同结构的复用组件
}
}
}
}
@Reusable
@Component
struct ReusableComponent {
@Require @Prop flag: boolean = true;
aboutToAppear() {
hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableComponent aboutToAppear');
}
aboutToReuse(params: ESObject) {
hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableComponent aboutToReuse');
this.flag = params.flag;
}
build() {
Column({ space: 5 }) {
Text('ReusableComponent')
if (this.flag) {
ReusableChildA()
} else {
ReusableChildB()
}
}.border({ width: 1 })
}
}
@Component
struct ReusableChildA {
aboutToAppear() {
hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableChildA aboutToAppear');
}
aboutToReuse() {
hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableChildA aboutToReuse');
}
build() {
Text('ReusableChildA')
.border({ width: 1 })
}
}
@Component
struct ReusableChildB {
aboutToAppear() {
hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableChildB aboutToAppear');
}
aboutToReuse() {
hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableChildB aboutToReuse');
}
build() {
Text('ReusableChildB')
.border({ width: 1 })
}
}
不支持ComponentContent
ComponentContent不支持传入@Reusable装饰器装饰的自定义组件。
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,触发复用。
// xxx.ets
export class Message {
public 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<string, ESObject>) {
this.message = params.message as Message;
}
build() {
Column() {
Text(this.message.value)
.fontSize(30)
}
.borderWidth(1)
.height(100)
}
}
列表滚动配合LazyForEach使用
当应用展示大量数据的列表并进行滚动操作时,频繁创建和销毁列表项视图可能导致卡顿和性能问题。使用列表组件的组件复用机制可以重用已创建的列表项视图,提高滚动流畅度。
以下示例代码将CardView自定义组件标记为复用组件,List上下滑动,触发CardView复用。
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++) { // 循环1000次
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<string, Object>): 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语句的删除和重新创建逻辑,提高复用效率和性能。
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG = '[Sample_ReusableComponent]';
const DOMAIN = 0xF811;
const BUNDLE = 'ReusableComponent_';
@Entry
@Component
struct Index {
private dataSource = new MyDataSource<FriendMoment>();
aboutToAppear(): void {
for (let i = 0; i < 20; i++) { // 循环20次
let title = i + 1 + 'test_if';
// 请开发者自行在src/main/resources/base/media路径下添加app.media.app_icon图片,否则运行时会因资源缺失而Image空白。
this.dataSource.pushData(new FriendMoment(i.toString(), title, 'app.media.app_icon'));
}
for (let i = 0; i < 50; i++) { // 循环50次
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 {
public id: string = '';
public text: string = '';
public title: string = '';
public image: string = '';
public answers: Array<ResourceStr> = [];
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 {
hilog.info(DOMAIN, TAG, BUNDLE + '=====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<T> 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<T> extends BasicDataSource<T> {
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复用成功,因为一帧内重复创建多个已被销毁的自定义组件。
// xxx.ets
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG = '[Sample_ReusableComponent]';
const DOMAIN = 0xF811;
const BUNDLE = 'ReusableComponent_';
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++) { // 循环100次
this.data.pushData(i.toString());
}
for (let i = 30; i <= 80; i++) { // 循环50次
this.data02.pushData(i.toString());
}
}
build() {
Column() {
Row() {
Button('clear').onClick(() => {
for (let i = 1; i <= 50; i++) { // 循环50次
this.dataSource.pop();
}
}).height(40)
Button('update').onClick(() => {
for (let i = 1; i <= 50; i++) { // 循环50次
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折叠展开属性,无法复用。
hilog.info(DOMAIN, TAG, BUNDLE + '=====aboutToAppear=====ListItemView==创建了==' + this.item);
}
aboutToReuse(params: ESObject) {
this.item = params.item;
// 点击clear,再次update,复用成功。
// 符合一帧内重复创建多个已被销毁的自定义组件。
hilog.info(DOMAIN, TAG, BUNDLE + '=====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 {
public uuid: string = '';
public id: number = 0;
public isExpand: boolean = false;
}
Grid使用场景
示例中使用@Reusable装饰器修饰GridItem中的自定义组件ReusableChildComponent,即表示其具备组件复用的能力。
使用aboutToReuse可以在Grid滑动时,从复用缓存中加入到组件树之前触发,从而更新组件状态变量,展示正确内容。
需要注意的是无需在aboutToReuse中对@Link、@StorageLink、@ObjectLink、@Consume等自动更新值的状态变量进行更新,可能触发不必要的组件刷新。
// 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++) { // 循环1000次
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<string, number>) {
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装饰器修饰,实现组件复用。
import { hilog } from '@kit.PerformanceAnalysisKit'; const TAG = '[Sample_ReusableComponent]'; const DOMAIN = 0xF811; const BUNDLE = 'ReusableComponent_'; class WaterFlowDataSource implements IDataSource { private dataArray: number[] = []; private listeners: DataChangeListener[] = []; constructor() { for (let i = 0; i < 60; i++) { // 循环60次 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; hilog.info(DOMAIN, TAG, BUNDLE + '=====aboutToReuse====FlowItem==复用了==' + this.item); } aboutToRecycle(): void { hilog.info(DOMAIN, TAG, BUNDLE + '=====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; // 最小值50 @State maxSize: number = 80; // 最大值80 @State fontSize: number = 24; // 字体大小为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++) { // 循环100次 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()) { // 阈值为20 for (let i = 0; i < 50; i++) { // 循环50次 this.dataSource.addLastItem(); } } }) }) } } } } }
Swiper使用场景
-
在Swiper滑动场景中,条目中的子组件频繁创建和销毁。可以将这些子组件封装成自定义组件,并使用@Reusable装饰器修饰,以实现组件复用。
@Entry @Component struct Index { private dataSource = new MyDataSource<Question>(); aboutToAppear(): void { for (let i = 0; i < 1000; i++) { // 循环1000次 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 { public id: string = ''; public title: ResourceStr = ''; public image: ResourceStr = ''; public answers: Array<ResourceStr> = []; constructor(id: string, title: ResourceStr, image: ResourceStr, answers: Array<ResourceStr>) { this.id = id; this.title = title; this.image = image; this.answers = answers; } } @Reusable @Component struct QuestionSwiperItem { @State itemData: Question | null = null; aboutToReuse(params: Record<string, Object>): void { this.itemData = params.itemData as Question; } 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<T> 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<T> extends BasicDataSource<T> { 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装饰器修饰,使其具备组件复用能力。
@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++) { // 循环10000次 let data1 = new DataSrc1(); for (let j = 0; j < 12; j++) { // 循环12次 data1.data.push(`测试条目数据: ${i} - ${j}`); } this.data.data.push(data1); } } 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 { public listeners: DataChangeListener[] = []; public 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 { public listeners: DataChangeListener[] = []; public 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或使用两个自定义组件来实现复用。
class LimitedMyDataSource 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 LimitedIndex {
private data: LimitedMyDataSource = new LimitedMyDataSource();
aboutToAppear() {
for (let i = 0; i < 1000; i++) { // 循环1000次
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之下。复用这些子组件时,缓存池在父组件层面实现共享,减少组件创建过程中的资源消耗。
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG = '[Sample_ReusableComponent]';
const DOMAIN = 0xF811;
const BUNDLE = 'ReusableComponent_';
const NUMBER3 = 3;
const NUMBER5 = 5;
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++) { // 循环1000次
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 % NUMBER3 === 0) {
this.itemBuilderOne(item);
} else if (index % NUMBER5 === 0) {
this.itemBuilderTwo(item);
} else {
this.itemBuilderThree(item);
}
}
.backgroundColor('#cccccc')
.width('100%')
.onAppear(() => {
hilog.info(DOMAIN, TAG, BUNDLE + `ListItem ${index} onAppear`);
})
}, (item: number) => item.toString())
}
.width('100%')
.height('100%')
.cachedCount(0)
}
}
@Reusable
@Component
struct ChildComponentA {
@State item: string = '';
aboutToReuse(params: ESObject) {
hilog.info(DOMAIN, TAG, BUNDLE + `ChildComponentA ${params.item} Reuse ${this.item}`);
this.item = params.item;
}
aboutToRecycle(): void {
hilog.info(DOMAIN, TAG, BUNDLE + `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 })
}
}