鸿蒙 @Reusable 组件复用详解

适用版本:HarmonyOS NEXT / API 12+
语言:ArkTS


一、为什么需要 @Reusable?

在长列表(List / WaterFlow / Grid)场景中,默认行为是:

item 滚出屏幕 → 组件销毁(aboutToDisappear)
item 滚回屏幕 → 组件重新创建(aboutToAppear → build → onDidBuild)

每次创建组件都要经历:对象分配 → 状态初始化 → 布局测量 → 渲染,列表快速滑动时这个开销会导致明显卡顿。

@Reusable 的作用:

item 滚出屏幕 → 组件放入复用池(aboutToRecycle)
item 需要显示 → 从池中取出并更新数据(aboutToReuse → build)

跳过了对象创建和首次初始化,只做数据更新,性能显著提升


二、核心机制

┌──────────────────────────────────────────────────┐
│                   复用池(Pool)                   │
│   [ ProductCard ] [ ProductCard ] [ ProductCard ] │
└──────────────┬───────────────────────┬────────────┘
               │ 取出(aboutToReuse)   │ 放入(aboutToRecycle)
               ▼                       ▲
┌──────────────────────────────────────────────────┐
│                  屏幕可视区域                      │
│  ┌────────────┐ ┌────────────┐ ┌────────────┐    │
│  │ 苹果 ¥5.0  │ │ 橙子 ¥8.5  │ │ 葡萄 ¥15   │    │
│  └────────────┘ └────────────┘ └────────────┘    │
└──────────────────────────────────────────────────┘

关键规则:

  • 相同 reuseId 的组件才能互相复用
  • 复用池的容量由 List.cachedCount 控制
  • 复用时 aboutToAppear 不会再次调用

三、生命周期详解

3.1 生命周期函数一览

生命周期 触发时机 调用次数
aboutToAppear 组件首次创建 仅 1 次
onDidBuild 首次 build 完成后 仅 1 次
aboutToReuse 从复用池取出、准备使用 每次复用都调用
aboutToRecycle 组件即将放入复用池 每次回收都调用
aboutToDisappear 组件真正销毁 仅 1 次

3.2 完整触发时序

首次创建:
  aboutToAppear() → build() → onDidBuild()

滚出屏幕(回收入池):
  aboutToRecycle()

从池中取出(复用):
  aboutToReuse(新参数) → build()

真正销毁(页面关闭 / 列表数据删除):
  aboutToDisappear()

3.3 代码示意

@Reusable
@Component
struct ProductCard {
  @State name: string = '';
  @State price: number = 0;
  @State imageUrl: string = '';
  private timer: number = -1;

  /** 首次创建时调用,只执行一次 */
  aboutToAppear(): void {
    console.info('[ProductCard] 首次创建');
    // 适合:注册事件监听、启动长驻定时器
    this.timer = setInterval(() => {
      this.refreshPrice();
    }, 5000);
  }

  /** 首次渲染完成,只执行一次 */
  onDidBuild(): void {
    console.info('[ProductCard] 首次渲染完成,可做入场动画');
  }

  /**
   * 从复用池取出时调用
   * ⚠️ 必须在这里重置所有会展示到 UI 上的状态
   */
  aboutToReuse(params: Record<string, Object>): void {
    console.info('[ProductCard] 复用,更新数据');
    this.name = params['name'] as string;
    this.price = params['price'] as number;
    this.imageUrl = params['imageUrl'] as string;
  }

  /**
   * 放入复用池前调用
   * 适合:暂停动画、取消网络请求、重置临时状态
   * ❌ 不要在这里清理定时器(复用后还需要用)
   */
  aboutToRecycle(): void {
    console.info('[ProductCard] 回收入池');
    // 暂停播放动画等轻量操作
  }

  /** 真正销毁时调用 */
  aboutToDisappear(): void {
    console.info('[ProductCard] 真正销毁,清理资源');
    clearInterval(this.timer); // 定时器在真正销毁时才清理
  }

  private refreshPrice(): void { /* 刷新价格 */ }

  build() {
    Column({ space: 8 }) {
      Image(this.imageUrl).width('100%').height(120).objectFit(ImageFit.Cover)
      Text(this.name).fontSize(14).fontColor('#333')
      Text(`¥${this.price.toFixed(2)}`).fontSize(16).fontColor('#FF5722').fontWeight(FontWeight.Bold)
    }
    .width('100%').padding(12)
    .backgroundColor(Color.White).borderRadius(8)
  }
}

四、配合 LazyForEach 使用

@Reusable 必须配合懒加载(LazyForEach)才能发挥最大效果。普通 ForEach 会一次性渲染所有节点,没有复用的必要。

4.1 实现 IDataSource

// 商品数据模型
class Product {
  id: string = '';
  name: string = '';
  price: number = 0;
  imageUrl: string = '';
  type: string = 'normal'; // 用于区分不同类型 item
}

// 数据源(LazyForEach 必须)
class ProductDataSource implements IDataSource {
  private list: Product[] = [];
  private listeners: DataChangeListener[] = [];

  constructor(data: Product[]) {
    this.list = data;
  }

  totalCount(): number {
    return this.list.length;
  }

  getData(index: number): Product {
    return this.list[index];
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    this.listeners.push(listener);
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    const idx = this.listeners.indexOf(listener);
    if (idx >= 0) this.listeners.splice(idx, 1);
  }

  // 追加数据(上拉加载更多)
  appendData(newData: Product[]): void {
    this.list.push(...newData);
    this.listeners.forEach(l => l.onDataReloaded());
  }
}

4.2 列表页完整示例

@Entry
@Component
struct ProductListPage {
  private dataSource: ProductDataSource = new ProductDataSource([
    { id: '1', name: '苹果', price: 5.0, imageUrl: '/img/apple.jpg', type: 'normal' },
    { id: '2', name: '橙子', price: 8.5, imageUrl: '/img/orange.jpg', type: 'normal' },
    { id: '3', name: '新品推荐', price: 0, imageUrl: '/img/banner.jpg', type: 'banner' },
    // ... 更多数据
  ]);

  build() {
    Column() {
      Text('商品列表').fontSize(20).fontWeight(FontWeight.Bold).padding(16)

      List({ space: 12 }) {
        LazyForEach(this.dataSource, (item: Product) => {
          ListItem() {
            // 根据 type 渲染不同组件,并用 reuseId 区分复用池
            if (item.type === 'banner') {
              BannerCard({ imageUrl: item.imageUrl })
                .reuseId('banner')          // banner 类型独立复用池
            } else {
              ProductCard({
                name: item.name,
                price: item.price,
                imageUrl: item.imageUrl
              })
              .reuseId('product')           // 普通商品独立复用池
            }
          }
        }, (item: Product) => item.id)      // key 必须唯一且稳定
      }
      .width('100%')
      .layoutWeight(1)
      .cachedCount(5)                       // 复用池最大缓存 5 个
      .divider({ strokeWidth: 1, color: '#F0F0F0' })
    }
    .width('100%').height('100%').backgroundColor('#F5F5F5')
  }
}

五、WaterFlow 瀑布流场景

电商 App 的瀑布流商品墙,是 @Reusable 最典型的应用场景:

@Reusable
@Component
struct WaterFlowCard {
  @State name: string = '';
  @State price: number = 0;
  @State imageUrl: string = '';
  @State imageHeight: number = 120; // 图片高度不同,形成错落感

  aboutToReuse(params: Record<string, Object>): void {
    this.name = params['name'] as string;
    this.price = params['price'] as number;
    this.imageUrl = params['imageUrl'] as string;
    this.imageHeight = params['imageHeight'] as number;
  }

  build() {
    Column({ space: 6 }) {
      Image(this.imageUrl)
        .width('100%')
        .height(this.imageHeight)  // 高度各异,形成瀑布流效果
        .objectFit(ImageFit.Cover)
        .borderRadius({ topLeft: 8, topRight: 8 })

      Column({ space: 4 }) {
        Text(this.name).fontSize(13).maxLines(2).textOverflow({ overflow: TextOverflow.Ellipsis })
        Text(`¥${this.price.toFixed(2)}`).fontSize(15).fontColor('#FF5722').fontWeight(FontWeight.Bold)
      }
      .padding({ left: 8, right: 8, bottom: 10 })
      .alignItems(HorizontalAlign.Start)
    }
    .backgroundColor(Color.White).borderRadius(8)
  }
}

@Entry
@Component
struct WaterFlowPage {
  private dataSource: ProductDataSource = new ProductDataSource([/* 数据 */]);

  build() {
    WaterFlow() {
      LazyForEach(this.dataSource, (item: Product) => {
        FlowItem() {
          WaterFlowCard({
            name: item.name,
            price: item.price,
            imageUrl: item.imageUrl,
            imageHeight: item.imageHeight
          })
          .reuseId('waterflow-card')
        }
      }, (item: Product) => item.id)
    }
    .columnsTemplate('1fr 1fr')  // 两列
    .columnsGap(8)
    .rowsGap(8)
    .padding(8)
    .cachedCount(6)
  }
}

六、多类型 item 的 reuseId 管理

当列表有多种样式时,用枚举统一管理 reuseId,避免字符串写错:

// constants/ReuseIds.ets
export const ReuseIds = {
  PRODUCT_NORMAL: 'product_normal',   // 普通商品卡片
  PRODUCT_FEATURED: 'product_featured', // 精选商品卡片(大图)
  BANNER: 'banner',                   // 横幅广告
  AD_SMALL: 'ad_small',              // 小广告
  SECTION_HEADER: 'section_header',   // 分组标题
};
// 使用枚举
LazyForEach(this.dataSource, (item: FeedItem) => {
  ListItem() {
    if (item.type === 'banner') {
      BannerCard({ data: item }).reuseId(ReuseIds.BANNER)
    } else if (item.type === 'featured') {
      FeaturedCard({ data: item }).reuseId(ReuseIds.PRODUCT_FEATURED)
    } else if (item.type === 'header') {
      SectionHeader({ title: item.title }).reuseId(ReuseIds.SECTION_HEADER)
    } else {
      NormalCard({ data: item }).reuseId(ReuseIds.PRODUCT_NORMAL)
    }
  }
}, (item: FeedItem) => item.id)

七、嵌套复用:子组件也加 @Reusable

当可复用组件内部包含开销较大的子组件时,子组件也应加 @Reusable,否则内层每次仍会重建:

// ✅ 外层和内层都加 @Reusable
@Reusable
@Component
struct ProductCard {           // 外层:可复用
  @State data: Product = new Product();

  aboutToReuse(params: Record<string, Object>): void {
    this.data = params['data'] as Product;
  }

  build() {
    Column() {
      ProductImage({ url: this.data.imageUrl }) // 内层也是 @Reusable
      ProductInfo({ data: this.data })
    }
  }
}

@Reusable
@Component
struct ProductImage {          // 内层:也标记可复用
  @Prop url: string = '';

  aboutToReuse(params: Record<string, Object>): void {
    this.url = params['url'] as string;
  }

  build() {
    Image(this.url).width('100%').height(180).objectFit(ImageFit.Cover)
  }
}

八、@Reusable 与 @ReusableV2 的区别

对比 @Reusable(V1) @ReusableV2(V2)
配合装饰器 @Component @ComponentV2
参数更新方式 aboutToReuse(params: Record<string, Object>) 通过 @Param 自动同步,无需手动赋值
复用触发 手动在 aboutToReuse 里赋值 框架自动把新 @Param 注入
代码量 较多(需手动处理每个字段) 较少(自动处理)

@ReusableV2 示例(推荐新项目使用):

@ReusableV2
@ComponentV2
struct ProductCardV2 {
  // @Param 在复用时由框架自动更新,无需写 aboutToReuse
  @Param name: string = '';
  @Param price: number = 0;
  @Param imageUrl: string = '';

  // 仍可监听复用/回收事件
  aboutToReuse(): void {
    console.info('V2复用,@Param 已自动更新');
  }

  aboutToRecycle(): void {
    console.info('V2回收入池');
  }

  build() {
    Column() {
      Image(this.imageUrl).width('100%').height(120)
      Text(this.name).fontSize(14)
      Text(`¥${this.price.toFixed(2)}`).fontColor('#FF5722')
    }
    .backgroundColor(Color.White).borderRadius(8).padding(12)
  }
}

九、常见坑点

❌ 坑 1:aboutToReuse 没有重置所有展示数据

// ❌ 忘记更新 imageUrl,复用时显示上一个 item 的图片
aboutToReuse(params: Record<string, Object>): void {
  this.name = params['name'] as string;
  this.price = params['price'] as number;
  // 漏掉了:this.imageUrl = params['imageUrl'] as string;
}

规则:凡是在 build() 中用到的 @State 变量,都必须在 aboutToReuse 中更新。


❌ 坑 2:在 aboutToRecycle 里清理定时器

// ❌ 错误:进池不代表销毁,回来还需要定时器
aboutToRecycle(): void {
  clearInterval(this.timer); // 复用后 timer 就消失了
}

// ✅ 正确:定时器只在真正销毁时清理
aboutToDisappear(): void {
  clearInterval(this.timer);
}

❌ 坑 3:LazyForEach 的 key 不唯一或不稳定

// ❌ 用 index 做 key,数据增删时会导致错误复用
LazyForEach(this.dataSource, (item: Product, index: number) => {
  ProductCard({ data: item }).reuseId('card')
}, (item: Product, index: number) => index.toString()) // ❌

// ✅ 用数据唯一标识做 key
LazyForEach(this.dataSource, (item: Product) => {
  ProductCard({ data: item }).reuseId('card')
}, (item: Product) => item.id) // ✅

❌ 坑 4:不同类型 item 用同一个 reuseId

// ❌ banner 和 card 共用一个 reuseId,布局混乱
BannerItem({ data: item }).reuseId('card')
ProductCard({ data: item }).reuseId('card')

// ✅ 不同类型用不同 reuseId
BannerItem({ data: item }).reuseId('banner')
ProductCard({ data: item }).reuseId('card')

❌ 坑 5:在普通 ForEach 中用 @Reusable(无效)

// ❌ ForEach 一次性渲染所有节点,@Reusable 不会触发复用机制
ForEach(this.products, (item: Product) => {
  ProductCard({ data: item }) // @Reusable 在这里没有意义
})

// ✅ 必须配合 LazyForEach / WaterFlow / Grid 的懒加载才有效
LazyForEach(this.dataSource, (item: Product) => {
  ProductCard({ data: item }).reuseId('card')
})

十、性能对比总结

场景 不用 @Reusable 用 @Reusable
滑动 1000 条列表 频繁创建销毁,掉帧明显 复用已有实例,丝滑流畅
内存占用 随列表长度线性增长 恒定(复用池大小固定)
首次渲染 相同 相同(首次仍需创建)
代码复杂度 略高(需处理 aboutToReuse)

十一、总结

@Reusable 核心三点:

1. 声明:@Reusable + @Component(或 @ReusableV2 + @ComponentV2)

2. 数据更新:aboutToReuse() 中重置所有 @State 数据
             (V2 的 @Param 自动更新,不用手动写)

3. 资源管理:定时器/订阅在 aboutToDisappear 清理
             动画/网络请求可在 aboutToRecycle 暂停

使用场景:LazyForEach 长列表 / WaterFlow 瀑布流 / Grid 网格
不适合:普通 ForEach(无复用机制)/ 固定少量元素的页面
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐