网站续费通知单,厦门网站建设官网,中天建设集团有限公司董事长,电子商务是建网站HarmonyOS 组件复用 指南
什么是组件复用#xff1f;
#x1f3af; 简单理解
想象一下#xff0c;你在玩积木。当你不再需要某个积木时#xff0c;你不会把它扔掉#xff0c;而是放到一个盒子里。下次需要相同类型的积木时#xff0c;你直接从盒子里拿出来用就行了。 …HarmonyOS 组件复用 指南
什么是组件复用 简单理解
想象一下你在玩积木。当你不再需要某个积木时你不会把它扔掉而是放到一个盒子里。下次需要相同类型的积木时你直接从盒子里拿出来用就行了。
组件复用就是这个道理
组件 界面上的一个部分比如列表中的一项复用 重复使用而不是重新创建缓存池 存放积木的盒子 技术定义
组件复用是指自定义组件从组件树上移除后被放入缓存池后续在创建相同类型的组件节点时直接复用缓存池中的组件对象。 为什么需要组件复用 性能优势
没有组件复用时
用户滑动列表 → 创建新组件 → 显示内容 → 滑出屏幕 → 销毁组件↑ ↓耗时耗内存 浪费资源有组件复用时
用户滑动列表 → 从缓存取组件 → 更新内容 → 显示 → 滑出屏幕 → 放入缓存↑ ↓快速高效 循环利用实际效果
✅ 减少内存回收频率✅ 降低 CPU 计算开销✅ 提升滑动流畅度✅ 改善用户体验 典型应用场景 长列表滑动如朋友圈、商品列表 界面切换频繁的场景 数据展示类应用 任何需要频繁创建/销毁组件的场景 组件复用的基本原理 复用流程图解 三个关键步骤
标记阶段给组件打上 Reusable 标签回收阶段组件滑出屏幕时放入缓存池复用阶段需要新组件时从缓存池取出并更新数据 关键概念解释
概念简单理解技术含义Reusable给组件贴上可重复使用的标签装饰器标记组件可复用reuseId给不同类型的组件分类存放复用标识区分缓存池aboutToReuse()组件重新上岗时的准备工作生命周期回调处理数据更新缓存池存放待复用组件的仓库CachedRecycleNodes 集合 入门实践基础列表复用 场景一相同结构的列表项
这是最简单的复用场景列表中每一项都长得一样只是内容不同。 ️ 实现步骤
第 1 步创建可复用组件
Reusable // 关键标记组件可复用
Component
struct ItemView {State title: string State content: string // 关键实现复用回调aboutToReuse(params: Recordstring, 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: Arrayany [{ 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: Recordstring, 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: Recordstring, 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: Arrayany [{ 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: Recordstring, 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: Recordstring, Object): void {this.imageUrl params.imageUrl as string}build() {Image(this.imageUrl).width(100).height(100)}
}// 多图组件
Reusable
Component
struct MultiImageComponent {State imageList: Arraystring []aboutToReuse(params: Recordstring, Object): void {this.imageList params.imageList as Arraystring}build() {Row() {ForEach(this.imageList, (url: string) {Image(url).width(60).height(60).margin(2)})}}
}// 底部时间组件
Reusable
Component
struct TimeComponent {State time: string aboutToReuse(params: Recordstring, 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: Mapstring, ArrayNodeItem 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: stringProp data: ObjectProp 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: ArrayItemData []build() {List() {LazyForEach(this.dataSource, (item: ItemData) {ListItem() {ReusableItemWrapper({type: news_item,data: item,builder: wrapBuilder(listItemBuilder)})}})}}
}Component
struct HotPage {State hotList: ArrayItemData []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: Arrayanyprivate currentIndex: number 0constructor(data: Arrayany) {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.currentIndexremainingTime - 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: Recordstring, 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: AttributeUpdaterTextAttribute new AttributeUpdater()aboutToReuse(params: Recordstring, 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: Recordstring, Object): void {this.item params.item as ItemData}
}✅ 推荐的做法
Observed
class ItemData {title: string content: string
}Reusable
Component
struct ItemView {ObjectLink item: ItemData // 共享引用自动同步aboutToReuse(params: Recordstring, 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: Recordstring, 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: Recordstring, 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: Recordstring, 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 三方库