当前位置: 首页 > news >正文

HarmonyOS 组件复用 指南

HarmonyOS 组件复用 指南

什么是组件复用?

🎯 简单理解

想象一下,你在玩积木。当你不再需要某个积木时,你不会把它扔掉,而是放到一个盒子里。下次需要相同类型的积木时,你直接从盒子里拿出来用就行了。

组件复用就是这个道理:

  • 组件 = 界面上的一个部分(比如列表中的一项)
  • 复用 = 重复使用,而不是重新创建
  • 缓存池 = 存放"积木"的盒子

🔍 技术定义

组件复用是指自定义组件从组件树上移除后被放入缓存池,后续在创建相同类型的组件节点时,直接复用缓存池中的组件对象。


为什么需要组件复用?

🚀 性能优势

没有组件复用时:

用户滑动列表 → 创建新组件 → 显示内容 → 滑出屏幕 → 销毁组件↑                              ↓耗时耗内存                        浪费资源

有组件复用时:

用户滑动列表 → 从缓存取组件 → 更新内容 → 显示 → 滑出屏幕 → 放入缓存↑                                    ↓快速高效                              循环利用

📊 实际效果

  • ✅ 减少内存回收频率
  • ✅ 降低 CPU 计算开销
  • ✅ 提升滑动流畅度
  • ✅ 改善用户体验

🎮 典型应用场景

  • 📱 长列表滑动(如朋友圈、商品列表)
  • 🔄 界面切换频繁的场景
  • 📊 数据展示类应用
  • 🎯 任何需要频繁创建/销毁组件的场景

组件复用的基本原理

🔄 复用流程图解

组件复用流程图

📝 三个关键步骤

  1. 标记阶段:给组件打上 @Reusable 标签
  2. 回收阶段:组件滑出屏幕时,放入缓存池
  3. 复用阶段:需要新组件时,从缓存池取出并更新数据

🎯 关键概念解释

概念简单理解技术含义
@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})}}
}
💡 新手提示
  1. @Reusable 是必须的,没有它就没有复用效果
  2. aboutToReuse() 是数据更新的地方,不要忘记实现
  3. 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}}})}}
}
💡 新手提示
  1. 不同类型的组件需要不同的 reuseId
  2. 每种类型都有自己的缓存池
  3. 数据结构要考虑类型区分

🎯 场景三:组件内部可拆分复用

有时候组件内部的某些部分可以单独复用,这样可以更精细地控制复用。

组件拆分复用

🛠️ 实现步骤

第 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)}}})}}
}
💡 新手提示
  1. 为什么用 @Builder 而不是直接嵌套组件?

    • 直接嵌套会分割缓存池,导致复用失效
    • @Builder 可以保持所有组件在同一个缓存池
  2. 细粒度复用的好处

    • 标题组件可以在所有类型间复用
    • 时间组件也可以在所有类型间复用
    • 提高了复用效率

进阶实践:复杂场景应用

🎯 场景:多个列表间的组件复用

当应用有多个页面,每个页面都有列表,我们希望不同页面的列表项也能互相复用。

多列表复用

🤔 问题分析

普通的组件复用只能在同一个父组件内生效。但是不同页面的列表是不同的父组件,所以需要创建一个全局的复用缓存池。

🛠️ 解决方案

第 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)})}})}}
}
💡 新手提示
  1. 全局复用池比较复杂,建议先掌握基础复用
  2. 可以使用现成的三方库:nodepool
  3. 注意控制缓存池大小,避免内存占用过多

🚀 性能优化:使用 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)}
}
💡 新手提示
  1. 预创建要适量:创建太多会占用内存
  2. 根据实际需求调整:统计常用的组件类型
  3. 监控性能:确保预创建本身不影响性能

高级技巧:性能优化

🎯 技巧一:使用 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. 合理选择复用粒度
  • 粗粒度复用:整个列表项作为一个复用单位
    • 优点:简单易用
    • 缺点:复用率可能不高
  • 细粒度复用:将列表项拆分成多个小组件
    • 优点:复用率高,性能更好
    • 缺点:代码复杂度增加

❓ 最佳实践总结

✅ 应该做的事情
  1. 优先使用基础复用:从简单场景开始
  2. 合理设置 reuseId:相同类型用相同 ID
  3. 及时更新数据:在 aboutToReuse 中处理
  4. 控制缓存大小:避免内存泄漏
  5. 性能监控:定期检查复用效果
❌ 不应该做的事情
  1. 嵌套 @Reusable 组件:会导致缓存池分割
  2. 频繁改变组件结构:影响复用效率
  3. 忽略内存管理:可能导致内存泄漏
  4. 过度优化:简单场景不需要复杂的复用逻辑

🎯 实战练习

练习 1:基础列表复用

创建一个简单的联系人列表,实现基础的组件复用。

需求

  • 显示联系人姓名和电话
  • 支持滑动浏览
  • 实现组件复用优化

提示

  • 使用 @Reusable 装饰器
  • 实现 aboutToReuse 方法
  • 设置合适的 reuseId

练习 2:多类型列表复用

创建一个新闻列表,包含文本、图片、视频三种类型。

需求

  • 支持多种类型的新闻条目
  • 不同类型有不同的布局
  • 实现类型间的复用优化

提示

  • 为不同类型创建不同组件
  • 使用不同的 reuseId 区分
  • 根据数据类型选择合适的组件

练习 3:全局复用池

实现一个多页面应用,不同页面的列表项可以互相复用。

需求

  • 多个页面都有相似的列表
  • 页面切换时能复用组件
  • 实现全局复用池管理

提示

  • 使用 NodePool 管理全局缓存
  • 实现 NodeController 包装组件
  • 合理控制缓存大小

📖 参考资料

  • HarmonyOS 官方文档 - 组件复用
  • ArkTS 开发指南 - @Reusable 装饰器
  • 性能优化最佳实践
  • NodePool 三方库

相关文章:

  • [直播推流] 使用 librtmp 库推流
  • 开心灿烂go开发面试题
  • C++/OpenCV地砖识别系统结合 Libevent 实现网络化 AI 接入
  • 雪豹速清APP:高效清理,畅享流畅手机体验
  • 【C++进阶篇】哈希的应用(位图)
  • 代码随想录算法训练营day4
  • Verilog自适应位宽与指定位宽不同导致模块无法正常执行
  • CMake 构建系统概述
  • CAD中DWG到DXF文件解析(一)
  • Linux入门(十六)shellshell变量设置环境变量位置参数变量预定义变量
  • langchain_mcp_adapters - MultiServerMCPClient 获取工具
  • 全球化2.0|云轴科技ZStack联合Teleplex举办技术沙龙 · 吉隆坡站
  • Flask文件上传与异常处理完全指南
  • 【Qt】QStateMachine状态机-对状态机分组、历史状态,实现复杂状态机
  • Git命令与代码仓库管理
  • qt中自定义控件编译成动态库
  • RabbitMQ核心函数的参数意义和使用场景
  • 深入解析JVM字节码执行引擎
  • SpringBoot自动化部署实战指南
  • 5、ZYNQ PL 点灯--流水灯
  • 怎么给做的网站做百度搜索/深圳哪里有网络推广渠避
  • 营销网站报备/软文广告经典案例300字
  • 上海 网站平台开发/网站关键词推广优化
  • 关于做网站的毕业设计/海南百度推广中心
  • 网站怎么产品做推广/网络推广与营销
  • 微信公众号对接网站如何做/自己建网站怎样建