HarmonyOS 组件复用 指南
HarmonyOS 组件复用 指南
什么是组件复用?
🎯 简单理解
想象一下,你在玩积木。当你不再需要某个积木时,你不会把它扔掉,而是放到一个盒子里。下次需要相同类型的积木时,你直接从盒子里拿出来用就行了。
组件复用就是这个道理:
- 组件 = 界面上的一个部分(比如列表中的一项)
- 复用 = 重复使用,而不是重新创建
- 缓存池 = 存放"积木"的盒子
🔍 技术定义
组件复用是指自定义组件从组件树上移除后被放入缓存池,后续在创建相同类型的组件节点时,直接复用缓存池中的组件对象。
为什么需要组件复用?
🚀 性能优势
没有组件复用时:
用户滑动列表 → 创建新组件 → 显示内容 → 滑出屏幕 → 销毁组件↑ ↓耗时耗内存 浪费资源
有组件复用时:
用户滑动列表 → 从缓存取组件 → 更新内容 → 显示 → 滑出屏幕 → 放入缓存↑ ↓快速高效 循环利用
📊 实际效果
- ✅ 减少内存回收频率
- ✅ 降低 CPU 计算开销
- ✅ 提升滑动流畅度
- ✅ 改善用户体验
🎮 典型应用场景
- 📱 长列表滑动(如朋友圈、商品列表)
- 🔄 界面切换频繁的场景
- 📊 数据展示类应用
- 🎯 任何需要频繁创建/销毁组件的场景
组件复用的基本原理
🔄 复用流程图解
📝 三个关键步骤
- 标记阶段:给组件打上
@Reusable
标签 - 回收阶段:组件滑出屏幕时,放入缓存池
- 复用阶段:需要新组件时,从缓存池取出并更新数据
🎯 关键概念解释
概念 | 简单理解 | 技术含义 |
---|---|---|
@Reusable | 给组件贴上"可重复使用"的标签 | 装饰器,标记组件可复用 |
reuseId | 给不同类型的组件分类存放 | 复用标识,区分缓存池 |
aboutToReuse() | 组件"重新上岗"时的准备工作 | 生命周期回调,处理数据更新 |
缓存池 | 存放待复用组件的"仓库" | CachedRecycleNodes 集合 |
入门实践:基础列表复用
🎯 场景一:相同结构的列表项
这是最简单的复用场景,列表中每一项都长得一样,只是内容不同。
🛠️ 实现步骤
第 1 步:创建可复用组件
@Reusable // 👈 关键:标记组件可复用
@Component
struct ItemView {@State title: string = ''@State content: string = ''// 👈 关键:实现复用回调aboutToReuse(params: Record<string, Object>): void {// 组件从缓存池取出时,更新数据this.title = params.title as stringthis.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 步:在列表中使用
@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 步:创建不同类型的组件
// 文本类型组件
@Reusable
@Component
struct TextItemView {@State title: string = ''@State content: string = ''aboutToReuse(params: Record<string, Object>): void {this.title = params.title as stringthis.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 stringthis.imageUrl = params.imageUrl as string}build() {Column() {Text(this.title).fontSize(16)Image(this.imageUrl).width(100).height(100)}}
}
第 2 步:根据数据类型选择组件
@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 步:拆分为可复用的子组件
// 标题组件
@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 组合不同类型
@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 步:创建全局复用池
// 单例模式的全局复用池
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 步:创建节点包装类
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 步:创建复用组件包装器
@Component
struct ReusableItemWrapper {@Prop type: string@Prop data: Object@Prop builder: WrappedBuilder<[Object]>private nodeItem: NodeItem | null = nullaboutToAppear() {// 从全局复用池获取节点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 步:在列表中使用
// 定义列表项视图
@Builder
function listItemBuilder(data: Object) {let item = data as ItemDataColumn() {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() 预创建组件
🤔 问题
首次进入页面时,由于缓存池为空,所有组件都需要重新创建,可能导致卡顿。
💡 解决方案
利用每帧的空闲时间,提前创建一些组件放入缓存池。
class IdleCallback extends FrameCallback {private preCreateData: Array<any>private currentIndex: number = 0constructor(data: Array<any>) {super()this.preCreateData = data}onIdle(idleTimeInNano: number): void {// 假设每个组件预创建耗时 1ms = 1000000nsconst singleComponentTime = 1000000let remainingTime = idleTimeInNanowhile (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 只更新需要改变的属性。
❌ 不推荐的做法
@Reusable
@Component
struct ItemView {@State title: string = ''@State fontColor: Color = Color.BlackaboutToReuse(params: Record<string, Object>): void {this.title = params.title as stringthis.fontColor = params.fontColor as Color // 👈 导致整个组件刷新}build() {Text(this.title).fontColor(this.fontColor).fontSize(16)}
}
✅ 推荐的做法
@Reusable
@Component
struct ItemView {@State title: string = ''@State fontColor: Color = Color.Blackprivate 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 共享数据引用。
❌ 不推荐的做法
@Reusable
@Component
struct ItemView {@Prop item: ItemData // 👈 会进行深拷贝aboutToReuse(params: Record<string, Object>): void {this.item = params.item as ItemData}
}
✅ 推荐的做法
@Observed
class ItemData {title: string = ''content: string = ''
}@Reusable
@Component
struct ItemView {@ObjectLink item: ItemData // 👈 共享引用,自动同步aboutToReuse(params: Record<string, Object>): void {// 👈 不需要重新赋值,数据会自动同步}
}
🎯 技巧三:合理使用 reuseId 区分组件
🤔 问题
组件内部使用 if/else 切换布局时,可能导致组件结构变化,影响复用效果。
💡 解决方案
为不同的布局分支设置不同的 reuseId。
❌ 不推荐的做法
@Reusable
@Component
struct ItemView {@State hasImage: boolean = falsebuild() {Column() {Text('标题')if (this.hasImage) {Flex() { // 👈 结构变化时,可能需要重新创建Image('image.png')}}}}
}
✅ 推荐的做法
@Reusable
@Component
struct ItemView {@State hasImage: boolean = falsebuild() {Column() {Text('标题')if (this.hasImage) {Flex() {Image('image.png')}.reuseId('with_image') // 👈 不同布局用不同ID} else {Flex().reuseId('without_image') // 👈 不同布局用不同ID}}}
}
🎯 技巧四:避免函数作为组件参数
🤔 问题
函数作为参数时,每次复用都会重新执行,造成性能损耗。
💡 解决方案
提前计算结果,通过状态变量传递。
❌ 不推荐的做法
@Reusable
@Component
struct ItemView {@Prop sum: number = 0aboutToReuse(params: Record<string, Object>): void {// 👈 每次复用都会执行这个耗时函数this.sum = (params.calculator as Function)()}
}// 使用时
ItemView({ sum: this.countAndReturn() }) // 👈 耗时函数
✅ 推荐的做法
@Component
struct ParentView {@State calculatedSum: number = 0aboutToAppear() {// 👈 只在初始化时计算一次this.calculatedSum = this.countAndReturn()}build() {List() {LazyForEach(this.dataSource, (item: any) => {ListItem() {ItemView({ sum: this.calculatedSum }) // 👈 直接传递结果}})}}
}
常见问题解答
❓ 如何检查组件复用是否生效?
方法一:使用 Code Linter 工具
# 在 DevEco Studio 中
1. 打开 Tools > Code Linter
2. 关注 @performance/hp-arkui-use-reusable-component 规则
3. 查看代码检查结果
方法二:使用 Profiler 工具
# 在 DevEco Studio 中
1. 打开 Profiler 工具
2. 抓取 Trace 数据
3. 搜索组件名称
4. 查看是否有 "BuildRecycle" 字段
方法三:添加日志调试
@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 装饰器
// ❌ 错误
@Component
struct ItemView {// ...
}// ✅ 正确
@Reusable
@Component
struct ItemView {// ...
}
2. 没有实现 aboutToReuse 方法
// ❌ 错误
@Reusable
@Component
struct ItemView {// 没有 aboutToReuse 方法
}// ✅ 正确
@Reusable
@Component
struct ItemView {aboutToReuse(params: Record<string, Object>): void {// 实现数据更新逻辑}
}
3. 组件不在同一父组件下
// ❌ 错误:不同的父组件
@Component
struct PageA {build() {List() {// ItemView 在 PageA 下}}
}@Component
struct PageB {build() {List() {// ItemView 在 PageB 下,无法复用 PageA 的}}
}// ✅ 正确:在同一父组件下
@Component
struct ParentPage {build() {Swiper() {PageA()PageB()// 两个页面在同一父组件下}}
}
❓ 性能优化建议
1. 控制缓存池大小
class NodePool {private maxCacheSize: number = 20; // 👈 限制缓存数量recycleNode(nodeItem: NodeItem) {let cachedArray = this.cachedNodes.get(type);if (cachedArray && cachedArray.length >= this.maxCacheSize) {return; // 👈 超过限制就不缓存}// ... 正常缓存逻辑}
}
2. 监控内存使用
@Component
struct MainPage {aboutToAppear() {// 定期清理缓存setInterval(() => {NodePool.getInstance().clearCache()}, 300000) // 5分钟清理一次}
}
3. 合理选择复用粒度
- 粗粒度复用:整个列表项作为一个复用单位
- 优点:简单易用
- 缺点:复用率可能不高
- 细粒度复用:将列表项拆分成多个小组件
- 优点:复用率高,性能更好
- 缺点:代码复杂度增加
❓ 最佳实践总结
✅ 应该做的事情
- 优先使用基础复用:从简单场景开始
- 合理设置 reuseId:相同类型用相同 ID
- 及时更新数据:在 aboutToReuse 中处理
- 控制缓存大小:避免内存泄漏
- 性能监控:定期检查复用效果
❌ 不应该做的事情
- 嵌套 @Reusable 组件:会导致缓存池分割
- 频繁改变组件结构:影响复用效率
- 忽略内存管理:可能导致内存泄漏
- 过度优化:简单场景不需要复杂的复用逻辑
🎯 实战练习
练习 1:基础列表复用
创建一个简单的联系人列表,实现基础的组件复用。
需求:
- 显示联系人姓名和电话
- 支持滑动浏览
- 实现组件复用优化
提示:
- 使用 @Reusable 装饰器
- 实现 aboutToReuse 方法
- 设置合适的 reuseId
练习 2:多类型列表复用
创建一个新闻列表,包含文本、图片、视频三种类型。
需求:
- 支持多种类型的新闻条目
- 不同类型有不同的布局
- 实现类型间的复用优化
提示:
- 为不同类型创建不同组件
- 使用不同的 reuseId 区分
- 根据数据类型选择合适的组件
练习 3:全局复用池
实现一个多页面应用,不同页面的列表项可以互相复用。
需求:
- 多个页面都有相似的列表
- 页面切换时能复用组件
- 实现全局复用池管理
提示:
- 使用 NodePool 管理全局缓存
- 实现 NodeController 包装组件
- 合理控制缓存大小
📖 参考资料
- HarmonyOS 官方文档 - 组件复用
- ArkTS 开发指南 - @Reusable 装饰器
- 性能优化最佳实践
- NodePool 三方库