鸿蒙6.0应用开发——ArkUI组件复用@Resuable
【高心星出品】
文章目录
ArkUI组件复用
1. 组件复用概述
1.1 基本概念与价值
组件复用是指自定义组件从组件树上移除后被放入缓存池,后续在创建相同类型的组件节点时,直接复用缓存池中的组件对象。
核心价值:
- 性能优化:避免频繁创建和销毁对象,减少内存回收频率
- 效率提升:复用缓存组件直接绑定数据,降低计算开销
- 流畅体验:特别在长列表滑动场景中,确保界面流畅度
1.2 适用场景
组件复用适用于任何发生自定义组件销毁和再创建的场景:
- 滑动场景:List、Grid、WaterFlow、Swiper等容器中的频繁滑动
- 条件渲染切换:界面中反复切换的控制分支,且子组件树结构复杂
2. 同一列表内的组件复用
2.1 实现原理与机制
ArkUI通过@Reusable装饰器实现组件复用,其核心机制如下图所示:

图:@Reusable组件从移除到缓存再到复用的完整流程
工作流程:
- 标记为
@Reusable的组件在离开屏幕后,从组件树移除并放入CustomNode虚拟节点 - RecycleManager根据reuseId分组回收CustomNode,形成缓存池
- 新组件需要显示时,优先从缓存池中查找匹配的视图对象并重新绑定数据
2.2 基础开发步骤
实现组件复用需要遵循三个基本步骤:
// 1. 定义可复用组件
@Reusable
@Component
struct ReusableComponent {
@State text: string = ''
// 2. 实现复用回调
aboutToReuse(params: Record<string, Object>): void {
this.text = params.text as string;
}
build() {
// 组件构建逻辑
}
}
@Entry
@Component
struct Index {
@State switch: boolean = true
@State typeStr: string = 'typeA'
build() {
Column() {
// 3. 布局中使用并设置reuseId
if (this.switch) {
ReusableComponent({ text: this.typeStr })
.reuseId(this.typeStr) // 设置复用标识
}
}
}
}
关键注意事项:
@Reusable修饰的组件需要布局在同一个父自定义组件下才能实现缓存复用- 不建议在
@Reusable组件中嵌套使用另一个@Reusable组件 - 未设置
reuseId时,组件名会默认作为reuseId
2.3 场景一:列表项结构类型相同
当列表中所有项具有相同结构时,可以将整个列表项作为复用单位。

图:结构相同的列表项滑动复用示意图
实现方案:
@Component
export struct OneTypeItemPage {
private dataSource: DataSource // 数据源
build() {
NavDestination() {
Column() {
List() {
LazyForEach(this.dataSource, (item: ItemData) => {
// 使用相同reuseId标记同类组件
ItemView({ title: item.title, from: item.from, tail: item.tail })
.reuseId('item_id') // 相同类型的组件使用相同ID
}, (item: ItemData) => item.id.toString())
}
}
}
}
}
@Reusable
@Component
struct ItemView {
@State title: string | Resource = '';
@State from: string | Resource = '';
@State tail: string | Resource = '';
aboutToReuse(params: Record<string, Object>): void {
// 更新所有需要变化的数据
this.title = params.title as string;
this.from = params.from as string;
this.tail = params.tail as string;
}
build() {
// 统一的列表项布局
}
}
2.4 场景二:列表项结构类型不同
当列表中包含多种结构类型的项时,需要为每种类型分别设置复用逻辑。

图:文本、单图、多图等不同类型列表项的复用分组
实现方案:
@Component
export struct MultiTypeItemPage {
private dataSource: DataSource
build() {
NavDestination() {
Column() {
List() {
LazyForEach(this.dataSource, (item: ItemData) => {
// 根据数据类型选择不同组件类型
if (item.type === 0) {
TextTypeItemView({ item: item })
.reuseId('text_item_id') // 文本类型ID
} else if (item.type === 1) {
ImageTypeItemView({ item: item })
.reuseId('image_item_id') // 图片类型ID
} else if (item.type === 2) {
ThreeImageTypeItemView({ item: item })
.reuseId('three_image_item_id') // 多图类型ID
}
}, (item: ItemData) => item.id.toString())
}
}
}
}
}
// 不同类型的组件分别定义
@Reusable
@Component
struct TextTypeItemView { /* ... */ }
@Reusable
@Component
struct ImageTypeItemView { /* ... */ }
@Reusable
@Component
struct ThreeImageTypeItemView { /* ... */ }
2.5 场景三:列表项内子组件可拆分组合
当列表项有共同部分和差异部分时,可以将子组件拆分,通过组合实现不同类型。

图:通过顶部、中部、底部子组件组合成不同类型的列表项
实现关键:使用@Builder而非嵌套自定义组件,确保所有可复用组件位于同一缓存池。
@Component
export struct ComposableItemPage {
// 使用@Builder组合子组件
@Builder
itemBuilderSingleImage(item: ItemData) {
TopView({ item: item }).reuseId('top_id')
MiddleSingleImageView({ item: item }).reuseId('middle_image_id')
BottomView({ item: item }).reuseId('bottom_id')
}
@Builder
itemBuilderThreeImage(item: ItemData) {
TopView({ item: item }).reuseId('top_id')
MiddleThreeImageView({ item: item }).reuseId('middle_three_image_id')
BottomView({ item: item }).reuseId('bottom_id')
}
build() {
NavDestination() {
Column() {
List() {
LazyForEach(this.dataSource, (item: ItemData) => {
ListItem() {
Column() {
// 根据类型选择不同的Builder组合
if (item.type === 0) {
this.itemBuilderSingleImage(item)
} else if (item.type === 1) {
this.itemBuilderThreeImage(item)
}
}
}
}, (item: ItemData) => item.id.toString())
}
}
}
}
}
// 定义各个可复用的子组件
@Reusable
@Component
struct TopView { /* ... */ }
@Reusable
@Component
struct BottomView { /* ... */ }
@Reusable
@Component
struct MiddleSingleImageView { /* ... */ }
为什么使用@Builder:缓存池位于自定义组件上,嵌套子组件会分割缓存池导致复用失效。@Builder可以使内部自定义组件汇聚在同一缓存池。
3. 多个列表间的组件复用
3.1 场景与挑战
在Swiper+List实现的页签切换场景中,不同页面的列表可能包含结构相同的列表项,但默认机制无法跨页面复用。

图:News、Hot等不同页签下相同结构列表项的跨列表复用
技术挑战:每个列表项的父组件是各自的List,当Swiper切换页面时,无法直接复用上一个页面的列表项。
为什么选择Swiper+List而非Tabs+List:
- Tabs内容页不支持
LazyForEach(),只能使用ForEach+TabContent - TabContent切换时不会执行
aboutToDisappear(),无法回收组件 - Swiper+List组合提供更精细的生命周期控制
3.2 自定义全局复用缓存池方案
通过自定义NodePool工具类,利用BuilderNode的节点复用能力实现跨列表组件复用。

图:NodePool全局管理节点创建、回收和复用的完整架构
3.2.1 实现节点控制器NodeItem
export class NodeItem extends NodeController {
public builder: WrappedBuilder<ESObject> | null = null;
public node: BuilderNode<ESObject> | null = null;
public data: ESObject = {};
public type: string = '';
public id: number = 0;
// 组件消失时回收到缓存池
aboutToDisappear(): void {
NodePool.getInstance().recycleNode(this.type, this);
}
// 更新节点数据
update(data: ESObject) {
this.data = data;
this.node?.reuse(data);
}
// 创建或更新节点
makeNode(uiContext: UIContext): FrameNode | null {
if (!this.node) {
// 新建节点
this.node = new BuilderNode(uiContext);
this.node.build(this.builder, this.data);
} else {
// 复用已有节点
this.update(this.data);
}
return this.node.getFrameNode();
}
}
3.2.2 实现全局缓存池NodePool
export class NodePool {
private static instance: NodePool;
private idGen: number;
private nodePool: HashMap<string, LinkedList<NodeItem>>;
private constructor() {
this.nodePool = new HashMap();
this.idGen = 0;
}
// 单例模式确保全局唯一缓存池
public static getInstance() {
if (!NodePool.instance) {
NodePool.instance = new NodePool();
}
return NodePool.instance;
}
// 获取可复用节点
public getNode(type: string, item: ESObject,
builder: WrappedBuilder<ESObject>): NodeItem | undefined {
let nodeItem: NodeItem | undefined = undefined;
if (this.nodePool.get(type)) {
for (let i = 0; i < this.nodePool.get(type)?.length; i++) {
let tmpItem: NodeItem | undefined = this.nodePool.get(type)?.get(i);
// 关键:父节点为空表示节点可复用
if (!tmpItem.node?.getFrameNode()?.getParent()) {
nodeItem = tmpItem;
this.nodePool.get(type)?.removeByIndex(i);
break;
}
}
}
// 未找到可复用节点时新建
if (!nodeItem) {
nodeItem = new NodeItem();
nodeItem.builder = builder;
nodeItem.data = item;
nodeItem.type = type;
nodeItem.id = this.getNextId();
}
return nodeItem;
}
// 回收节点到缓存池
public recycleNode(type: string, node: NodeItem) {
// 重置节点属性,避免复用异常
node.data = {};
if (!this.nodePool.get(type)) {
this.nodePool.set(type, new LinkedList());
}
this.nodePool.get(type)?.add(node);
}
}
4. 总结
组件复用是优化ArkUI应用性能的关键技术,尤其对于包含长列表、复杂条件渲染的场景。通过合理运用@Reusable装饰器和aboutToReuse生命周期,可以显著提升界面流畅度和响应速度。
对于简单场景,内置的复用机制足以满足需求;对于复杂的跨列表复用场景,自定义NodePool方案提供了灵活的解决方案。开发者应根据实际业务需求选择合适的复用策略,在提升性能的同时确保代码的可维护性。
更多推荐




所有评论(0)