鸿蒙 @Reusable 组件复用详解
·
鸿蒙 @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(无复用机制)/ 固定少量元素的页面
更多推荐



所有评论(0)