HarmonyOS 组件复用 指南

HarmonyOS 组件复用 指南

什么是组件复用?

🎯 简单理解

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

组件复用就是这个道理:

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

🔍 技术定义

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


为什么需要组件复用?

🚀 性能优势

没有组件复用时:

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

有组件复用时:

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

📊 实际效果

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

🎮 典型应用场景

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

组件复用的基本原理

🔄 复用流程图解

📝 三个关键步骤

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

🎯 关键概念解释

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

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

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

🛠️ 实现步骤

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

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

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

进阶实践:复杂场景应用

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

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

🤔 问题分析

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

🛠️ 解决方案

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

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

高级技巧:性能优化

🎯 技巧一:使用 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)
  }
}
🤔 问题

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

❓ 最佳实践总结

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

🎯 实战练习

练习 1:基础列表复用

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

需求

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

提示

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

练习 2:多类型列表复用

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

需求

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

提示

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

练习 3:全局复用池

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

需求

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

提示

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

📖 参考资料

相关推荐
一只栖枝几秒前
华为 HCIE 大数据认证中 Linux 命令行的运用及价值
大数据·linux·运维·华为·华为认证·hcie·it
zhanshuo4 小时前
在鸿蒙里优雅地处理网络错误:从 Demo 到实战案例
harmonyos
zhanshuo4 小时前
在鸿蒙中实现深色/浅色模式切换:从原理到可运行 Demo
harmonyos
whysqwhw9 小时前
鸿蒙分布式投屏
harmonyos
whysqwhw11 小时前
鸿蒙AVSession Kit
harmonyos
whysqwhw12 小时前
鸿蒙各种生命周期
harmonyos
whysqwhw13 小时前
鸿蒙音频编码
harmonyos
whysqwhw13 小时前
鸿蒙音频解码
harmonyos
whysqwhw13 小时前
鸿蒙视频解码
harmonyos
whysqwhw14 小时前
鸿蒙视频编码
harmonyos