HarmonyOS 组件复用 指南
什么是组件复用?
🎯 简单理解
想象一下,你在玩积木。当你不再需要某个积木时,你不会把它扔掉,而是放到一个盒子里。下次需要相同类型的积木时,你直接从盒子里拿出来用就行了。
组件复用就是这个道理:
- 组件 = 界面上的一个部分(比如列表中的一项)
- 复用 = 重复使用,而不是重新创建
- 缓存池 = 存放"积木"的盒子
🔍 技术定义
组件复用是指自定义组件从组件树上移除后被放入缓存池,后续在创建相同类型的组件节点时,直接复用缓存池中的组件对象。
为什么需要组件复用?
🚀 性能优势
没有组件复用时:
用户滑动列表 → 创建新组件 → 显示内容 → 滑出屏幕 → 销毁组件
↑ ↓
耗时耗内存 浪费资源
有组件复用时:
用户滑动列表 → 从缓存取组件 → 更新内容 → 显示 → 滑出屏幕 → 放入缓存
↑ ↓
快速高效 循环利用
📊 实际效果
- ✅ 减少内存回收频率
- ✅ 降低 CPU 计算开销
- ✅ 提升滑动流畅度
- ✅ 改善用户体验
🎮 典型应用场景
- 📱 长列表滑动(如朋友圈、商品列表)
- 🔄 界面切换频繁的场景
- 📊 数据展示类应用
- 🎯 任何需要频繁创建/销毁组件的场景
组件复用的基本原理
🔄 复用流程图解

📝 三个关键步骤
- 标记阶段 :给组件打上
@Reusable
标签 - 回收阶段:组件滑出屏幕时,放入缓存池
- 复用阶段:需要新组件时,从缓存池取出并更新数据
🎯 关键概念解释
概念 | 简单理解 | 技术含义 |
---|---|---|
@Reusable | 给组件贴上"可重复使用"的标签 | 装饰器,标记组件可复用 |
reuseId | 给不同类型的组件分类存放 | 复用标识,区分缓存池 |
aboutToReuse() | 组件"重新上岗"时的准备工作 | 生命周期回调,处理数据更新 |
缓存池 | 存放待复用组件的"仓库" | CachedRecycleNodes 集合 |
入门实践:基础列表复用
🎯 场景一:相同结构的列表项
这是最简单的复用场景,列表中每一项都长得一样,只是内容不同。

🛠️ 实现步骤
第 1 步:创建可复用组件
typescript
@Reusable // 👈 关键:标记组件可复用
@Component
struct ItemView {
@State title: string = ''
@State content: string = ''
// 👈 关键:实现复用回调
aboutToReuse(params: Record<string, Object>): void {
// 组件从缓存池取出时,更新数据
this.title = params.title as string
this.content = params.content as string
}
build() {
Column() {
Text(this.title)
.fontSize(16)
.fontWeight(FontWeight.Bold)
Text(this.content)
.fontSize(14)
.fontColor(Color.Gray)
}
.padding(10)
}
}
第 2 步:在列表中使用
typescript
@Component
struct ContentPage {
@State dataList: Array<any> = [
{ title: '标题1', content: '内容1' },
{ title: '标题2', content: '内容2' },
// ... 更多数据
]
build() {
List() {
LazyForEach(this.dataSource, (item: any) => {
ListItem() {
ItemView({ title: item.title, content: item.content })
}
.reuseId('item_view') // 👈 关键:设置复用ID
})
}
}
}
💡 新手提示
- @Reusable 是必须的,没有它就没有复用效果
- aboutToReuse() 是数据更新的地方,不要忘记实现
- reuseId 用来区分不同类型的组件,相同类型用相同 ID
🎯 场景二:不同结构的列表项
当列表中有多种不同类型的条目时,比如有些是纯文本,有些带图片,有些是视频。

🛠️ 实现步骤
第 1 步:创建不同类型的组件
typescript
// 文本类型组件
@Reusable
@Component
struct TextItemView {
@State title: string = ''
@State content: string = ''
aboutToReuse(params: Record<string, Object>): void {
this.title = params.title as string
this.content = params.content as string
}
build() {
Column() {
Text(this.title).fontSize(16)
Text(this.content).fontSize(14)
}
}
}
// 图片类型组件
@Reusable
@Component
struct ImageItemView {
@State title: string = ''
@State imageUrl: string = ''
aboutToReuse(params: Record<string, Object>): void {
this.title = params.title as string
this.imageUrl = params.imageUrl as string
}
build() {
Column() {
Text(this.title).fontSize(16)
Image(this.imageUrl)
.width(100)
.height(100)
}
}
}
第 2 步:根据数据类型选择组件
typescript
@Component
struct ContentPage {
@State dataList: Array<any> = [
{ type: 'text', title: '纯文本', content: '这是内容' },
{ type: 'image', title: '带图片', imageUrl: 'path/to/image' },
// ... 更多数据
]
build() {
List() {
LazyForEach(this.dataSource, (item: any) => {
ListItem() {
if (item.type === 'text') {
TextItemView({ title: item.title, content: item.content })
.reuseId('text_item') // 👈 不同类型用不同ID
} else if (item.type === 'image') {
ImageItemView({ title: item.title, imageUrl: item.imageUrl })
.reuseId('image_item') // 👈 不同类型用不同ID
}
}
})
}
}
}
💡 新手提示
- 不同类型的组件需要不同的 reuseId
- 每种类型都有自己的缓存池
- 数据结构要考虑类型区分
🎯 场景三:组件内部可拆分复用
有时候组件内部的某些部分可以单独复用,这样可以更精细地控制复用。

🛠️ 实现步骤
第 1 步:拆分为可复用的子组件
typescript
// 标题组件
@Reusable
@Component
struct TitleComponent {
@State title: string = ''
aboutToReuse(params: Record<string, Object>): void {
this.title = params.title as string
}
build() {
Text(this.title)
.fontSize(16)
.fontWeight(FontWeight.Bold)
}
}
// 单图组件
@Reusable
@Component
struct SingleImageComponent {
@State imageUrl: string = ''
aboutToReuse(params: Record<string, Object>): void {
this.imageUrl = params.imageUrl as string
}
build() {
Image(this.imageUrl)
.width(100)
.height(100)
}
}
// 多图组件
@Reusable
@Component
struct MultiImageComponent {
@State imageList: Array<string> = []
aboutToReuse(params: Record<string, Object>): void {
this.imageList = params.imageList as Array<string>
}
build() {
Row() {
ForEach(this.imageList, (url: string) => {
Image(url)
.width(60)
.height(60)
.margin(2)
})
}
}
}
// 底部时间组件
@Reusable
@Component
struct TimeComponent {
@State time: string = ''
aboutToReuse(params: Record<string, Object>): void {
this.time = params.time as string
}
build() {
Text(this.time)
.fontSize(12)
.fontColor(Color.Gray)
}
}
第 2 步:用 @Builder 组合不同类型
typescript
@Component
struct ContentPage {
// 文本类型布局
@Builder textItemBuilder(item: any) {
Column() {
TitleComponent({ title: item.title })
.reuseId('title_component')
TimeComponent({ time: item.time })
.reuseId('time_component')
}
}
// 单图类型布局
@Builder singleImageItemBuilder(item: any) {
Column() {
TitleComponent({ title: item.title })
.reuseId('title_component')
SingleImageComponent({ imageUrl: item.imageUrl })
.reuseId('single_image_component')
TimeComponent({ time: item.time })
.reuseId('time_component')
}
}
// 多图类型布局
@Builder multiImageItemBuilder(item: any) {
Column() {
TitleComponent({ title: item.title })
.reuseId('title_component')
MultiImageComponent({ imageList: item.imageList })
.reuseId('multi_image_component')
TimeComponent({ time: item.time })
.reuseId('time_component')
}
}
build() {
List() {
LazyForEach(this.dataSource, (item: any) => {
ListItem() {
if (item.type === 'text') {
this.textItemBuilder(item)
} else if (item.type === 'single_image') {
this.singleImageItemBuilder(item)
} else if (item.type === 'multi_image') {
this.multiImageItemBuilder(item)
}
}
})
}
}
}
💡 新手提示
-
为什么用 @Builder 而不是直接嵌套组件?
- 直接嵌套会分割缓存池,导致复用失效
- @Builder 可以保持所有组件在同一个缓存池
-
细粒度复用的好处
- 标题组件可以在所有类型间复用
- 时间组件也可以在所有类型间复用
- 提高了复用效率
进阶实践:复杂场景应用
🎯 场景:多个列表间的组件复用
当应用有多个页面,每个页面都有列表,我们希望不同页面的列表项也能互相复用。

🤔 问题分析
普通的组件复用只能在同一个父组件内生效。但是不同页面的列表是不同的父组件,所以需要创建一个全局的复用缓存池。
🛠️ 解决方案
第 1 步:创建全局复用池
typescript
// 单例模式的全局复用池
class NodePool {
private static instance: NodePool;
private cachedNodes: Map<string, Array<NodeItem>> = new Map();
static getInstance(): NodePool {
if (!NodePool.instance) {
NodePool.instance = new NodePool();
}
return NodePool.instance;
}
// 获取复用节点
getNode(
type: string,
builder: WrappedBuilder<[Object]>,
data: Object
): NodeItem {
let cachedArray = this.cachedNodes.get(type);
if (cachedArray && cachedArray.length > 0) {
// 从缓存中取出
let nodeItem = cachedArray.pop()!;
// 检查节点是否有效
if (nodeItem.getNodePtr() !== null) {
nodeItem.updateData(data);
return nodeItem;
}
}
// 创建新节点
let nodeItem = new NodeItem(type, builder);
nodeItem.updateData(data);
return nodeItem;
}
// 回收节点
recycleNode(nodeItem: NodeItem) {
let type = nodeItem.getType();
if (!this.cachedNodes.has(type)) {
this.cachedNodes.set(type, []);
}
// 重置节点状态
nodeItem.resetState();
this.cachedNodes.get(type)!.push(nodeItem);
}
}
第 2 步:创建节点包装类
typescript
class NodeItem extends NodeController {
private type: string;
private builder: WrappedBuilder<[Object]>;
private data: Object = {};
private node: BuilderNode<[Object]> | null = null;
constructor(type: string, builder: WrappedBuilder<[Object]>) {
super();
this.type = type;
this.builder = builder;
}
makeNode(uiContext: UIContext): FrameNode | null {
if (this.node === null) {
this.node = new BuilderNode(uiContext, { builder: this.builder });
}
// 更新数据
this.node.update(this.data);
return this.node.getFrameNode();
}
updateData(data: Object) {
this.data = data;
}
getType(): string {
return this.type;
}
resetState() {
// 重置状态,避免复用时显示异常
this.data = {};
}
}
第 3 步:创建复用组件包装器
typescript
@Component
struct ReusableItemWrapper {
@Prop type: string
@Prop data: Object
@Prop builder: WrappedBuilder<[Object]>
private nodeItem: NodeItem | null = null
aboutToAppear() {
// 从全局复用池获取节点
this.nodeItem = NodePool.getInstance().getNode(this.type, this.builder, this.data)
}
aboutToDisappear() {
// 回收到全局复用池
if (this.nodeItem) {
NodePool.getInstance().recycleNode(this.nodeItem)
}
}
build() {
NodeContainer(this.nodeItem)
.height(100)
}
}
第 4 步:在列表中使用
typescript
// 定义列表项视图
@Builder
function listItemBuilder(data: Object) {
let item = data as ItemData
Column() {
Text(item.title).fontSize(16)
Text(item.content).fontSize(14)
}
.padding(10)
}
@Component
struct NewsPage {
@State newsList: Array<ItemData> = []
build() {
List() {
LazyForEach(this.dataSource, (item: ItemData) => {
ListItem() {
ReusableItemWrapper({
type: 'news_item',
data: item,
builder: wrapBuilder(listItemBuilder)
})
}
})
}
}
}
@Component
struct HotPage {
@State hotList: Array<ItemData> = []
build() {
List() {
LazyForEach(this.dataSource, (item: ItemData) => {
ListItem() {
ReusableItemWrapper({
type: 'news_item', // 👈 相同类型可以复用
data: item,
builder: wrapBuilder(listItemBuilder)
})
}
})
}
}
}
💡 新手提示
- 全局复用池比较复杂,建议先掌握基础复用
- 可以使用现成的三方库:nodepool
- 注意控制缓存池大小,避免内存占用过多
🚀 性能优化:使用 onIdle() 预创建组件
🤔 问题
首次进入页面时,由于缓存池为空,所有组件都需要重新创建,可能导致卡顿。
💡 解决方案
利用每帧的空闲时间,提前创建一些组件放入缓存池。

typescript
class IdleCallback extends FrameCallback {
private preCreateData: Array<any>
private currentIndex: number = 0
constructor(data: Array<any>) {
super()
this.preCreateData = data
}
onIdle(idleTimeInNano: number): void {
// 假设每个组件预创建耗时 1ms = 1000000ns
const singleComponentTime = 1000000
let remainingTime = idleTimeInNano
while (remainingTime > singleComponentTime && this.currentIndex < this.preCreateData.length) {
// 预创建组件
let data = this.preCreateData[this.currentIndex]
NodePool.getInstance().preBuild(data.type, data.builder)
this.currentIndex++
remainingTime -= singleComponentTime
}
// 如果还有未创建的组件,继续下一帧
if (this.currentIndex < this.preCreateData.length) {
this.postFrameCallback(this)
}
}
}
// 使用方式
@Entry
@Component
struct MainPage {
aboutToAppear() {
// 页面加载时开始预创建
let preCreateData = [
{ type: 'news_item', builder: wrapBuilder(listItemBuilder) },
{ type: 'hot_item', builder: wrapBuilder(listItemBuilder) },
// ... 更多类型
]
let callback = new IdleCallback(preCreateData)
this.getUIContext().postFrameCallback(callback)
}
}
💡 新手提示
- 预创建要适量:创建太多会占用内存
- 根据实际需求调整:统计常用的组件类型
- 监控性能:确保预创建本身不影响性能
高级技巧:性能优化
🎯 技巧一:使用 attributeUpdater 精确刷新
🤔 问题
默认情况下,更新组件数据会导致整个组件重新渲染,即使只改变了一个属性。
💡 解决方案
使用 attributeUpdater 只更新需要改变的属性。
❌ 不推荐的做法
typescript
@Reusable
@Component
struct ItemView {
@State title: string = ''
@State fontColor: Color = Color.Black
aboutToReuse(params: Record<string, Object>): void {
this.title = params.title as string
this.fontColor = params.fontColor as Color // 👈 导致整个组件刷新
}
build() {
Text(this.title)
.fontColor(this.fontColor)
.fontSize(16)
}
}
✅ 推荐的做法
typescript
@Reusable
@Component
struct ItemView {
@State title: string = ''
@State fontColor: Color = Color.Black
private textUpdater: AttributeUpdater<TextAttribute> = new AttributeUpdater()
aboutToReuse(params: Record<string, Object>): void {
this.title = params.title as string
// 只更新颜色属性,不触发整个组件刷新
this.textUpdater.fontColor(params.fontColor as Color)
}
build() {
Text(this.title)
.attributeUpdater(this.textUpdater) // 👈 绑定属性更新器
.fontSize(16)
}
}
🎯 技巧二:使用 @Link/@ObjectLink 替代 @Prop
🤔 问题
@Prop 会进行深拷贝,增加创建时间和内存消耗。
💡 解决方案
使用 @Link 或 @ObjectLink 共享数据引用。
❌ 不推荐的做法
typescript
@Reusable
@Component
struct ItemView {
@Prop item: ItemData // 👈 会进行深拷贝
aboutToReuse(params: Record<string, Object>): void {
this.item = params.item as ItemData
}
}
✅ 推荐的做法
typescript
@Observed
class ItemData {
title: string = ''
content: string = ''
}
@Reusable
@Component
struct ItemView {
@ObjectLink item: ItemData // 👈 共享引用,自动同步
aboutToReuse(params: Record<string, Object>): void {
// 👈 不需要重新赋值,数据会自动同步
}
}
🎯 技巧三:合理使用 reuseId 区分组件
🤔 问题
组件内部使用 if/else 切换布局时,可能导致组件结构变化,影响复用效果。
💡 解决方案
为不同的布局分支设置不同的 reuseId。
❌ 不推荐的做法
typescript
@Reusable
@Component
struct ItemView {
@State hasImage: boolean = false
build() {
Column() {
Text('标题')
if (this.hasImage) {
Flex() { // 👈 结构变化时,可能需要重新创建
Image('image.png')
}
}
}
}
}
✅ 推荐的做法
typescript
@Reusable
@Component
struct ItemView {
@State hasImage: boolean = false
build() {
Column() {
Text('标题')
if (this.hasImage) {
Flex() {
Image('image.png')
}
.reuseId('with_image') // 👈 不同布局用不同ID
} else {
Flex()
.reuseId('without_image') // 👈 不同布局用不同ID
}
}
}
}
🎯 技巧四:避免函数作为组件参数
🤔 问题
函数作为参数时,每次复用都会重新执行,造成性能损耗。
💡 解决方案
提前计算结果,通过状态变量传递。
❌ 不推荐的做法
typescript
@Reusable
@Component
struct ItemView {
@Prop sum: number = 0
aboutToReuse(params: Record<string, Object>): void {
// 👈 每次复用都会执行这个耗时函数
this.sum = (params.calculator as Function)()
}
}
// 使用时
ItemView({ sum: this.countAndReturn() }) // 👈 耗时函数
✅ 推荐的做法
typescript
@Component
struct ParentView {
@State calculatedSum: number = 0
aboutToAppear() {
// 👈 只在初始化时计算一次
this.calculatedSum = this.countAndReturn()
}
build() {
List() {
LazyForEach(this.dataSource, (item: any) => {
ListItem() {
ItemView({ sum: this.calculatedSum }) // 👈 直接传递结果
}
})
}
}
}
常见问题解答
❓ 如何检查组件复用是否生效?
方法一:使用 Code Linter 工具
bash
# 在 DevEco Studio 中
1. 打开 Tools > Code Linter
2. 关注 @performance/hp-arkui-use-reusable-component 规则
3. 查看代码检查结果
方法二:使用 Profiler 工具
bash
# 在 DevEco Studio 中
1. 打开 Profiler 工具
2. 抓取 Trace 数据
3. 搜索组件名称
4. 查看是否有 "BuildRecycle" 字段
方法三:添加日志调试
typescript
@Reusable
@Component
struct ItemView {
@State title: string = ''
aboutToReuse(params: Record<string, Object>): void {
console.log('组件复用成功:', this.title, '->', params.title) // 👈 添加日志
this.title = params.title as string
}
aboutToAppear() {
console.log('创建新组件:', this.title) // 👈 添加日志
}
}
❓ 复用不生效的常见原因
1. 忘记添加 @Reusable 装饰器
typescript
// ❌ 错误
@Component
struct ItemView {
// ...
}
// ✅ 正确
@Reusable
@Component
struct ItemView {
// ...
}
2. 没有实现 aboutToReuse 方法
typescript
// ❌ 错误
@Reusable
@Component
struct ItemView {
// 没有 aboutToReuse 方法
}
// ✅ 正确
@Reusable
@Component
struct ItemView {
aboutToReuse(params: Record<string, Object>): void {
// 实现数据更新逻辑
}
}
3. 组件不在同一父组件下
typescript
// ❌ 错误:不同的父组件
@Component
struct PageA {
build() {
List() {
// ItemView 在 PageA 下
}
}
}
@Component
struct PageB {
build() {
List() {
// ItemView 在 PageB 下,无法复用 PageA 的
}
}
}
// ✅ 正确:在同一父组件下
@Component
struct ParentPage {
build() {
Swiper() {
PageA()
PageB()
// 两个页面在同一父组件下
}
}
}
❓ 性能优化建议
1. 控制缓存池大小
typescript
class NodePool {
private maxCacheSize: number = 20; // 👈 限制缓存数量
recycleNode(nodeItem: NodeItem) {
let cachedArray = this.cachedNodes.get(type);
if (cachedArray && cachedArray.length >= this.maxCacheSize) {
return; // 👈 超过限制就不缓存
}
// ... 正常缓存逻辑
}
}
2. 监控内存使用
typescript
@Component
struct MainPage {
aboutToAppear() {
// 定期清理缓存
setInterval(() => {
NodePool.getInstance().clearCache()
}, 300000) // 5分钟清理一次
}
}
3. 合理选择复用粒度
- 粗粒度复用 :整个列表项作为一个复用单位
- 优点:简单易用
- 缺点:复用率可能不高
- 细粒度复用 :将列表项拆分成多个小组件
- 优点:复用率高,性能更好
- 缺点:代码复杂度增加
❓ 最佳实践总结
✅ 应该做的事情
- 优先使用基础复用:从简单场景开始
- 合理设置 reuseId:相同类型用相同 ID
- 及时更新数据:在 aboutToReuse 中处理
- 控制缓存大小:避免内存泄漏
- 性能监控:定期检查复用效果
❌ 不应该做的事情
- 嵌套 @Reusable 组件:会导致缓存池分割
- 频繁改变组件结构:影响复用效率
- 忽略内存管理:可能导致内存泄漏
- 过度优化:简单场景不需要复杂的复用逻辑
🎯 实战练习
练习 1:基础列表复用
创建一个简单的联系人列表,实现基础的组件复用。
需求:
- 显示联系人姓名和电话
- 支持滑动浏览
- 实现组件复用优化
提示:
- 使用 @Reusable 装饰器
- 实现 aboutToReuse 方法
- 设置合适的 reuseId
练习 2:多类型列表复用
创建一个新闻列表,包含文本、图片、视频三种类型。
需求:
- 支持多种类型的新闻条目
- 不同类型有不同的布局
- 实现类型间的复用优化
提示:
- 为不同类型创建不同组件
- 使用不同的 reuseId 区分
- 根据数据类型选择合适的组件
练习 3:全局复用池
实现一个多页面应用,不同页面的列表项可以互相复用。
需求:
- 多个页面都有相似的列表
- 页面切换时能复用组件
- 实现全局复用池管理
提示:
- 使用 NodePool 管理全局缓存
- 实现 NodeController 包装组件
- 合理控制缓存大小