【高心星出品】

ArkUI组件复用

1. 组件复用概述

1.1 基本概念与价值

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

核心价值

  • 性能优化:避免频繁创建和销毁对象,减少内存回收频率
  • 效率提升:复用缓存组件直接绑定数据,降低计算开销
  • 流畅体验:特别在长列表滑动场景中,确保界面流畅度

1.2 适用场景

组件复用适用于任何发生自定义组件销毁和再创建的场景:

  1. 滑动场景:List、Grid、WaterFlow、Swiper等容器中的频繁滑动
  2. 条件渲染切换:界面中反复切换的控制分支,且子组件树结构复杂

2. 同一列表内的组件复用

2.1 实现原理与机制

ArkUI通过@Reusable装饰器实现组件复用,其核心机制如下图所示:

在这里插入图片描述

图:@Reusable组件从移除到缓存再到复用的完整流程

工作流程

  1. 标记为@Reusable的组件在离开屏幕后,从组件树移除并放入CustomNode虚拟节点
  2. RecycleManager根据reuseId分组回收CustomNode,形成缓存池
  3. 新组件需要显示时,优先从缓存池中查找匹配的视图对象并重新绑定数据

2.2 基础开发步骤

实现组件复用需要遵循三个基本步骤:

// 1. 定义可复用组件
@Reusable
@Component
struct ReusableComponent {
  @State text: string = ''
  
  // 2. 实现复用回调
  aboutToReuse(params: Record<string, Object>): void {
    this.text = params.text as string;
  }
  
  build() {
    // 组件构建逻辑
  }
}

@Entry
@Component
struct Index {
  @State switch: boolean = true
  @State typeStr: string = 'typeA'
  
  build() {
    Column() {
      // 3. 布局中使用并设置reuseId
      if (this.switch) {
        ReusableComponent({ text: this.typeStr })
          .reuseId(this.typeStr) // 设置复用标识
      }
    }
  }
}

关键注意事项

  • @Reusable修饰的组件需要布局在同一个父自定义组件下才能实现缓存复用
  • 不建议在@Reusable组件中嵌套使用另一个@Reusable组件
  • 未设置reuseId时,组件名会默认作为reuseId

2.3 场景一:列表项结构类型相同

当列表中所有项具有相同结构时,可以将整个列表项作为复用单位。

在这里插入图片描述

图:结构相同的列表项滑动复用示意图

实现方案

@Component
export struct OneTypeItemPage {
  private dataSource: DataSource // 数据源
  
  build() {
    NavDestination() {
      Column() {
        List() {
          LazyForEach(this.dataSource, (item: ItemData) => {
            // 使用相同reuseId标记同类组件
            ItemView({ title: item.title, from: item.from, tail: item.tail })
              .reuseId('item_id') // 相同类型的组件使用相同ID
          }, (item: ItemData) => item.id.toString())
        }
      }
    }
  }
}

@Reusable
@Component
struct ItemView {
  @State title: string | Resource = '';
  @State from: string | Resource = '';
  @State tail: string | Resource = '';
  
  aboutToReuse(params: Record<string, Object>): void {
    // 更新所有需要变化的数据
    this.title = params.title as string;
    this.from = params.from as string;
    this.tail = params.tail as string;
  }
  
  build() {
    // 统一的列表项布局
  }
}

2.4 场景二:列表项结构类型不同

当列表中包含多种结构类型的项时,需要为每种类型分别设置复用逻辑。

在这里插入图片描述

图:文本、单图、多图等不同类型列表项的复用分组

实现方案

@Component
export struct MultiTypeItemPage {
  private dataSource: DataSource
  
  build() {
    NavDestination() {
      Column() {
        List() {
          LazyForEach(this.dataSource, (item: ItemData) => {
            // 根据数据类型选择不同组件类型
            if (item.type === 0) {
              TextTypeItemView({ item: item })
                .reuseId('text_item_id') // 文本类型ID
            } else if (item.type === 1) {
              ImageTypeItemView({ item: item })
                .reuseId('image_item_id') // 图片类型ID
            } else if (item.type === 2) {
              ThreeImageTypeItemView({ item: item })
                .reuseId('three_image_item_id') // 多图类型ID
            }
          }, (item: ItemData) => item.id.toString())
        }
      }
    }
  }
}

// 不同类型的组件分别定义
@Reusable
@Component
struct TextTypeItemView { /* ... */ }

@Reusable
@Component
struct ImageTypeItemView { /* ... */ }

@Reusable
@Component
struct ThreeImageTypeItemView { /* ... */ }

2.5 场景三:列表项内子组件可拆分组合

当列表项有共同部分和差异部分时,可以将子组件拆分,通过组合实现不同类型。

在这里插入图片描述

图:通过顶部、中部、底部子组件组合成不同类型的列表项

实现关键:使用@Builder而非嵌套自定义组件,确保所有可复用组件位于同一缓存池。

@Component
export struct ComposableItemPage {
  // 使用@Builder组合子组件
  @Builder
  itemBuilderSingleImage(item: ItemData) {
    TopView({ item: item }).reuseId('top_id')
    MiddleSingleImageView({ item: item }).reuseId('middle_image_id')
    BottomView({ item: item }).reuseId('bottom_id')
  }
  
  @Builder
  itemBuilderThreeImage(item: ItemData) {
    TopView({ item: item }).reuseId('top_id')
    MiddleThreeImageView({ item: item }).reuseId('middle_three_image_id')
    BottomView({ item: item }).reuseId('bottom_id')
  }
  
  build() {
    NavDestination() {
      Column() {
        List() {
          LazyForEach(this.dataSource, (item: ItemData) => {
            ListItem() {
              Column() {
                // 根据类型选择不同的Builder组合
                if (item.type === 0) {
                  this.itemBuilderSingleImage(item)
                } else if (item.type === 1) {
                  this.itemBuilderThreeImage(item)
                }
              }
            }
          }, (item: ItemData) => item.id.toString())
        }
      }
    }
  }
}

// 定义各个可复用的子组件
@Reusable
@Component
struct TopView { /* ... */ }

@Reusable
@Component
struct BottomView { /* ... */ }

@Reusable
@Component
struct MiddleSingleImageView { /* ... */ }

为什么使用@Builder:缓存池位于自定义组件上,嵌套子组件会分割缓存池导致复用失效。@Builder可以使内部自定义组件汇聚在同一缓存池。

3. 多个列表间的组件复用

3.1 场景与挑战

在Swiper+List实现的页签切换场景中,不同页面的列表可能包含结构相同的列表项,但默认机制无法跨页面复用。

在这里插入图片描述

图:News、Hot等不同页签下相同结构列表项的跨列表复用

技术挑战:每个列表项的父组件是各自的List,当Swiper切换页面时,无法直接复用上一个页面的列表项。

为什么选择Swiper+List而非Tabs+List

  • Tabs内容页不支持LazyForEach(),只能使用ForEach+TabContent
  • TabContent切换时不会执行aboutToDisappear(),无法回收组件
  • Swiper+List组合提供更精细的生命周期控制

3.2 自定义全局复用缓存池方案

通过自定义NodePool工具类,利用BuilderNode的节点复用能力实现跨列表组件复用。

在这里插入图片描述

图:NodePool全局管理节点创建、回收和复用的完整架构

3.2.1 实现节点控制器NodeItem
export class NodeItem extends NodeController {
  public builder: WrappedBuilder<ESObject> | null = null;
  public node: BuilderNode<ESObject> | null = null;
  public data: ESObject = {};
  public type: string = '';
  public id: number = 0;
  
  // 组件消失时回收到缓存池
  aboutToDisappear(): void {
    NodePool.getInstance().recycleNode(this.type, this);
  }
  
  // 更新节点数据
  update(data: ESObject) {
    this.data = data;
    this.node?.reuse(data);
  }
  
  // 创建或更新节点
  makeNode(uiContext: UIContext): FrameNode | null {
    if (!this.node) {
      // 新建节点
      this.node = new BuilderNode(uiContext);
      this.node.build(this.builder, this.data);
    } else {
      // 复用已有节点
      this.update(this.data);
    }
    return this.node.getFrameNode();
  }
}
3.2.2 实现全局缓存池NodePool
export class NodePool {
  private static instance: NodePool;
  private idGen: number;
  private nodePool: HashMap<string, LinkedList<NodeItem>>;
  
  private constructor() {
    this.nodePool = new HashMap();
    this.idGen = 0;
  }
  
  // 单例模式确保全局唯一缓存池
  public static getInstance() {
    if (!NodePool.instance) {
      NodePool.instance = new NodePool();
    }
    return NodePool.instance;
  }
  
  // 获取可复用节点
  public getNode(type: string, item: ESObject, 
                 builder: WrappedBuilder<ESObject>): NodeItem | undefined {
    let nodeItem: NodeItem | undefined = undefined;
    
    if (this.nodePool.get(type)) {
      for (let i = 0; i < this.nodePool.get(type)?.length; i++) {
        let tmpItem: NodeItem | undefined = this.nodePool.get(type)?.get(i);
        // 关键:父节点为空表示节点可复用
        if (!tmpItem.node?.getFrameNode()?.getParent()) {
          nodeItem = tmpItem;
          this.nodePool.get(type)?.removeByIndex(i);
          break;
        }
      }
    }
    
    // 未找到可复用节点时新建
    if (!nodeItem) {
      nodeItem = new NodeItem();
      nodeItem.builder = builder;
      nodeItem.data = item;
      nodeItem.type = type;
      nodeItem.id = this.getNextId();
    }
    
    return nodeItem;
  }
  
  // 回收节点到缓存池
  public recycleNode(type: string, node: NodeItem) {
    // 重置节点属性,避免复用异常
    node.data = {};
    
    if (!this.nodePool.get(type)) {
      this.nodePool.set(type, new LinkedList());
    }
    this.nodePool.get(type)?.add(node);
  }
}

4. 总结

组件复用是优化ArkUI应用性能的关键技术,尤其对于包含长列表、复杂条件渲染的场景。通过合理运用@Reusable装饰器和aboutToReuse生命周期,可以显著提升界面流畅度和响应速度。

对于简单场景,内置的复用机制足以满足需求;对于复杂的跨列表复用场景,自定义NodePool方案提供了灵活的解决方案。开发者应根据实际业务需求选择合适的复用策略,在提升性能的同时确保代码的可维护性。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐