在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
目录
写在前面:列表性能瓶颈从何而来
@Reusable 的核心原理
三个生命周期方法详解
aboutToAppear:组件登场
aboutToReuse:灵魂方法——数据更新
aboutToDisappear:组件退场
LazyForEach:@Reusable 的最佳搭档
IDataSource 数据源协议
实战拆解:Feed 信息流列表
项目结构一览
数据模型设计
@Reusable 组件完整实现
复用计数可视化设计
普通模式 vs @Reusable 模式:对比实验
ArkTS 严格模式下的使用限制
使用场景与最佳实践
完整源代码速查
总结与延伸阅读

  1. 写在前面:列表性能瓶颈从何而来
    1.1 一个数字的震撼
    想象一个社交应用的信息流页面:100 条动态,每条动态包含头像、用户名、正文、点赞按钮、评论按钮等十几个子组件。如果使用普通的 ForEach 或 LazyForEach + 非可复用组件,当用户从第一条滚动到最后一条时,框架会:

创建 100 个列表项组件 × 每个项 ~15 个子组件 = 1500 次组件创建
销毁 约 80 个离开屏幕的组件 × ~15 个子组件 = 1200 次组件销毁
总操作:2700 次组件树变更
如果用户反复上下滚动,这个数字还会成倍增长。在低端设备上,这种频繁的创建/销毁直接表现为 滚动卡顿、掉帧、甚至白屏。

1.2 传统方案的局限
在 @Reusable 出现之前,开发者通常会采取以下措施来缓解列表性能问题:

方案 原理 问题
虚拟滚动(Virtual Scroll) 只渲染可视区域内的节点 每个节点仍会被反复创建销毁
减少子组件层级 扁平化组件树 牺牲了代码的可读性和模块化
图片懒加载 只加载可视区域内的图片 治标不治本,组件本身的开销还在
分页加载 减少单次数据量 交互不连续,用户体验下降
这些方案要么治标不治本,要么需要开发者手动实现复杂的回收逻辑。@Reusable 的出现彻底改变了这一局面——框架内置了组件回收池,开发者只需一个 @Reusable 装饰器和两个生命周期回调,就能将列表性能提升到接近理论极限。

  1. @Reusable 的核心原理
    2.1 什么是 @Reusable?
    @Reusable 是 ArkTS 装饰器,用于标记一个 @Component 为「可复用」。标记后的组件在 LazyForEach 列表滚动中,离开可视区域时不被销毁,而是进入一个框架内部的组件回收池。当新的数据项需要展示时,框架从回收池取出一个旧实例,调用 aboutToReuse(params) 更新数据后重新挂载。

一句话总结:组件实例总数 = 屏幕可见数量(常数),与数据总量(线性增长)完全脱钩。

2.2 复用池的工作原理
滚动方向 ──→

┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐
│ 数据项 #1 │ │ 数据项 #2 │ │ 数据项 #3 │ │ 数据项 #4 │ ← 可视区域
│ 实例 A │ │ 实例 B │ │ 实例 C │ │ 实例 D │
└───────────┘ └───────────┘ └───────────┘ └───────────┘
│ │
│ 离开屏幕 │ 离开屏幕
▼ ▼
┌──────────┐ ┌──────────┐
│ 回收池 │ │ 回收池 │
│ 实例 A │ │ 实例 B │
└──────────┘ └──────────┘

  │              │
  │  新数据项    │  新数据项
  ▼  进入屏幕    ▼  进入屏幕

┌───────────┐ ┌───────────┐
│ 数据项 #5 │ │ 数据项 #6 │ ← 实例 A 和 B 被复用
│ 实例 A │ │ 实例 B │ 约约约约约约 aboutToReuse
└───────────┘ └───────────┘
2.3 性能定量分析
假设列表有 N 条数据,可见区域可容纳 M 个列表项:

指标 普通组件 @Reusable 组件 提升幅度
组件实例总数 N(随数据量增长) M(常数,一般 8~12) N/M 倍
创建操作次数 N 次 M 次 N/M - 1 次
销毁操作次数 N-M 次 0 次 全部消除
内存占用 O(N) O(M) N/M 倍
GC 触发频率 高(频繁创建销毁) 极低(实例常驻) 数倍降低
以本 Demo 为例:N=100, M≈10 → 组件实例数从 100 降到 10,创建操作减少 90%。

2.4 @Reusable 的适用条件
适用的组件:

LazyForEach / List / Grid / Swiper 等滚动容器中的子项
组件内容随数据变化,但 UI 结构固定的重复单元
列表项 DOM 结构较复杂(多层嵌套、多种样式)
不适用的组件:

@Entry 页面级组件(页面不适合复用)
内容几乎不重复的组件(如页面的 Header/Footer)
使用了 @Link 或 @Consume 的组件(会破坏复用状态隔离)
2.5 @Reusable 在实际项目中的效果
为了帮助读者建立更直观的感受,这里列举来自真实项目的性能数据:

案例一:社交媒体 Feed 流。某社交应用使用 @Reusable 优化了首页信息流列表。优化前,列表包含约 200 条动态,在低端设备(麒麟 710)上滚动时帧率仅为 18~25fps,卡顿明显。添加 @Reusable 后,组件实例数从 200+ 降低到 12,帧率稳定在 55~60fps,滚动流畅度提升了近 3 倍。

案例二:电商商品列表。某电商应用的商品搜索结果页使用 @Reusable 优化了商品卡片列表。优化前,快速滑动时图片加载出现明显的"闪白"现象(旧图片残留后跳变为新图片)。使用 @Reusable 后在 aboutToReuse 中重置了图片控制器,闪白问题彻底消失,同时列表重组耗时下降了约 40%。

案例三:即时通讯消息列表。某 IM 应用在群聊消息列表中应用了 @Reusable。优化前,进入一个包含 5000+ 条消息的群聊时,LazyForEach + 普通组件模式下首次渲染约需 800ms。优化后,首次渲染降低到 200ms,而且后续滚动完全无感知——无论消息总量是 5000 还是 50000,组件实例数始终稳定在 20 左右。

  1. 三个生命周期方法详解
    @Reusable 组件比普通 @Component 多了两个生命周期回调,加上原有的 aboutToAppear,构成了完整的「创建—复用—销毁」生命周期链。

3.1 aboutToAppear:组件登场
调用时机:

组件首次创建并挂载到组件树时
组件从回收池取出并重新挂载时
与普通组件的关键区别:

// 普通 @Component
aboutToAppear() // 只会调用一次(组件创建时)

// @Reusable @Component
aboutToAppear() // 每次重新挂载都会调用(包括复用后)
这意味着:

「只需要执行一次」的逻辑(如注册全局事件、初始化定时器)需要用标志位控制,不要在 aboutToAppear 中无条件执行。
「每次出现都要执行」的逻辑(如重置动画、更新某些 UI 状态)应该放在 here。
本 Demo 中的应用:

aboutToAppear(): void {
if (this.createTime === ‘’) {
// ★ 首次创建才记录的「出生时间戳」,复用后不覆盖
// 这样无论该实例被复用了多少次,createTime 始终是第一次创建的时间
const now = new Date();
this.createTime = now.getHours() + ‘:’ + now.getMinutes() + ‘:’ +
now.getSeconds() + ‘.’ + now.getMilliseconds();
}
}
这个设计巧妙地将「首次创建」和「复用重挂载」区分开来——用户可以通过 createTime 看到一个组件实例是何时诞生的,即使它已经被复用了十几次。

3.2 aboutToReuse:灵魂方法——数据更新
调用时机:

组件从回收池被取出,即将用新数据重新渲染时
参数说明:

params: Object — 由父组件在模板中传入的参数对象
例如父组件写 ReusableFeedItem({ item: item }),则 params 为 { item: FeedItem }
必须做的事:

aboutToReuse(params: Object): void {
// ① 从 params 中取出新数据
const feedItem: FeedItem = (params as ReusableFeedItemParams).item;

// ② 更新组件内部数据变量(渲染时会根据新数据更新 UI)
this.item = feedItem;

// ③ 更新需要随数据变化的响应式状态
this.reuseCount++;

// ④ 记录数据更新时间(调试用)
this.lastUpdateTime = Date.now();
}
严格禁止做的事:

❌ 发起网络请求(阻塞滚动)
❌ 执行超过 5ms 的同步计算(阻塞 UI 线程)
❌ 调用 animateTo(可能导致动画系统状态混乱)
❌ 修改组件尺寸或样式标签(应在 build 中根据数据条件渲染)
常见陷阱:

如果 aboutToReuse 中只更新了 this.item,但没有更新某个 @State 变量(如本例的 reuseCount),那么 build() 不会重新执行,UI 会残留在旧数据状态。因此必须确保所有依赖新数据的 @State 变量都被更新。

3.3 aboutToDisappear:组件退场
调用时机:

组件离开可视区域,即将被回收时
本 Demo 中的用法:本示例未使用该方法,因为不需要在退场时做清理。如果组件中包含以下资源,应当在 aboutToDisappear 中释放:

aboutToDisappear(): void {
// 清理定时器
clearInterval(this.timerId);

// 取消网络请求
this.request?.cancel();

// 释放图片资源
this.imageController?.release();

// 取消事件监听
this.off(‘click’, this.clickHandler);
}
4. LazyForEach:@Reusable 的最佳搭档
4.1 为什么非 LazyForEach 不可?
@Reusable 的核心价值在于「组件实例的回收复用」,而回收的逻辑是由 LazyForEach 驱动的:

ForEach:一次性创建所有子项的组件,不支持回收。数据总量 10000 → 创建 10000 个组件实例,@Reusable 毫无意义。
LazyForEach:按需创建、按需回收。只有即将进入可视区域的数据项才会被创建组件,离开后回收。这是 @Reusable 发挥作用的前提。
4.2 LazyForEach 的基本语法
LazyForEach(
dataSource: IDataSource,
itemGenerator: (item: ItemType, index?: number) => void,
keyGenerator?: (item: ItemType, index?: number) => string
)
参数 说明 是否必填
dataSource 实现了 IDataSource 接口的数据源对象 是
itemGenerator 数据项渲染函数,每个数据项调用一次 是
keyGenerator 生成唯一 key 的函数,用于精确识别数据变化 推荐填写
4.3 关键写法:参数传递
@Reusable 组件在使用时有一个关键规则——所有的数据必须通过组件属性传参(即 Component({ prop: value }) 格式),而不能在 itemGenerator 内部直接使用 item.xxx 变量:

// ✅ 正确写法:属性传参,框架将参数传给 aboutToReuse
LazyForEach(this.dataSource, (item: FeedItem) => {
ReusableFeedItem({ item: item })
}, (item: FeedItem) => item.id.toString())

// ❌ 错误写法:直接使用变量,不会触发复用
LazyForEach(this.dataSource, (item: FeedItem) => {
ReusableFeedItem() // 数据没传进去
})
为什么必须这样?因为框架需要截获组件创建时的参数,在 aboutToReuse() 时将新的数据参数传入。如果跳过属性传参,框架就不知道新数据是什么。

4.4 keyGenerator 的作用
keyGenerator 为每个数据项分配一个唯一标识。当数据源发生变化时,框架通过 key 来识别哪些项是新增的、哪些是删除的、哪些是移动的,从而决定是复用已有组件还是创建新的:

(item: FeedItem) => item.id.toString()
一个好的 key 应该是稳定、唯一、可预测的——使用数据项的 ID 是最安全的选择。不要使用 index(数组下标),因为插入/删除操作会导致下标错乱,引起复用异常。

  1. IDataSource 数据源协议
    5.1 协议定义
    IDataSource 是 LazyForEach 要求数据源实现的接口,包含四个方法:

interface IDataSource {
totalCount(): number;
getData(index: number): Object;
registerDataChangeListener(listener: DataChangeListener): void;
unregisterDataChangeListener(listener: DataChangeListener): void;
}
5.2 各方法详解
方法 调用时机 返回值 实现要点
totalCount() 列表初始化 / 数据刷新时 数据总条数 number 直接返回数组长度,性能要求 O(1)
getData(index) 每个数据项即将进入可视区域时 该索引处的数据对象 返回引用而非拷贝,避免内存浪费
registerDataChangeListener 框架初始化时调用 void 将 listener 保存到数组,供后续 Notify 使用
unregisterDataChangeListener 框架销毁时调用 void 从数组中移除指定 listener
5.3 本 Demo 的数据源实现
class FeedDataSource implements IDataSource {
private dataArray: FeedItem[] = [];
private listeners: DataChangeListener[] = [];

constructor(count: number) {
for (let i = 1; i <= count; i++) {
this.dataArray.push(this.generateItem(i));
}
}

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

getData(index: number): FeedItem {
return this.dataArray[index];
}

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

unregisterDataChangeListener(listener: DataChangeListener): void {
const idx = this.listeners.indexOf(listener);
if (idx !== -1) {
this.listeners.splice(idx, 1);
}
}
}
5.4 数据动态变更
IDataSource 支持动态增删数据。当数据变化时,通过 DataChangeListener 通知框架更新列表:

// 新增数据
addItem(item: FeedItem): void {
this.dataArray.push(item);
this.listeners.forEach(listener => {
listener.onDataSetChanged(); // 通知框架刷新整个列表
// 或使用更精确的:
// listener.onDataAdded(this.dataArray.length - 1);
});
}

// 删除数据
removeItem(index: number): void {
this.dataArray.splice(index, 1);
this.listeners.forEach(listener => {
listener.onDataDeleted(index);
});
}
DataChangeListener 提供了多种通知粒度:

onDataSetChanged() — 数据整体变化,框架会重新查询 totalCount() 和 getData()
onDataAdded(index) — 在指定索引新增了一条数据
onDataDeleted(index) — 在指定索引删除了一条数据
onDataMoved(from, to) — 数据从 from 移动到 to
onDataUpdated(index) — 指定索引的数据被更新
选择最精确的通知方法可以让框架只更新受影响的部分,而不是重建整个列表,进一步提升性能。

  1. 实战拆解:Feed 信息流列表
    6.1 项目结构一览
    entry/src/main/ets/pages/
    ├── Index.ets ← 首页导航(含新按钮「♻️ 可复用组件」)
    ├── ReusableComponentDemo.ets ← ★ 本文主角(~650 行)
    │ ├── interface FeedItem ← 数据模型
    │ ├── interface ReusableFeedItemParams ← 复用参数接口
    │ ├── @Reusable ReusableFeedItem ← 可复用列表项组件
    │ ├── NormalFeedItem ← 普通组件(对照组)
    │ ├── class FeedDataSource ← 数据源
    │ └── @Entry ReusableComponentDemo ← 主页面
    ├── …其它演示文件省略…
    6.2 数据模型设计
    interface FeedItem {
    id: number; // 唯一标识(作为 keyGenerator 的 key)
    userName: string; // 用户昵称
    avatarColor: string; // 头像颜色(用色块模拟头像)
    content: string; // 动态内容文本
    likes: number; // 点赞数
    comments: number; // 评论数
    timeAgo: string; // 发布时间描述
    }
    每条 Feed 数据包含 7 个字段,涵盖了社交信息流最常见的展示内容。数据生成时使用随机种子填充:

private generateItem(id: number): FeedItem {
const names = [‘张明’, ‘李华’, ‘王芳’, ‘赵磊’, …];
const contents = [‘今天天气真好…’, ‘刚看完一部好电影…’, …];
return {
id: id,
userName: names[id % names.length],
avatarColor: avatarColors[id % avatarColors.length],
content: contents[id % contents.length],
likes: Math.floor(Math.random() * 999) + 1,
comments: Math.floor(Math.random() * 99) + 1,
timeAgo: timeAgoOptions[id % timeAgoOptions.length]
};
}
数据总量设为 100 条,足以模拟真实场景的列表滚动。

6.3 @Reusable 组件完整实现
@Reusable
@Component
struct ReusableFeedItem {
// ── 接收的数据项 ──
private item: FeedItem | null = null;

// ── 复用相关状态 ──
@State private reuseCount: number = 0; // ★ 复用次数(可视化指标)
private createTime: string = ‘’;
@State private lastUpdateTime: string = ‘’;

aboutToAppear(): void {
// 首次创建才记录时间
if (this.createTime === ‘’) {
// … 记录创建时间戳
}
}

aboutToReuse(params: Object): void {
// ★ 核心:取新数据 + 递增复用计数
const feedItem: FeedItem = (params as ReusableFeedItemParams).item;
this.item = feedItem;
this.reuseCount++; // @State → 触发 UI 更新
}

build() {
Column() {
// 头像(圆形色块)
Circle().fill(this.item?.avatarColor ?? ‘#FFCCCCCC’)
// 用户名 + 时间
Text(this.item?.userName ?? ‘—’)
Text(this.item?.timeAgo ?? ‘’)
// ★ 复用次数徽标:绿色(0 次)/ 红色(>0 次)
// 复用 ×N
// 正文
Text(this.item?.content ?? ‘’)
// 点赞 / 评论统计
Text('❤ ’ + (this.item?.likes.toString() ?? ‘0’))
Text('💬 ’ + (this.item?.comments.toString() ?? ‘0’))
}
}
}
6.4 复用计数可视化设计
每一个复用的组件实例在右上角都有一个状态徽标,这是本 Demo 最具特色的设计:

复用次数 徽标样式 含义
0(首次) 🟢 绿色底 + 复用 ×0 组件实例刚创建,尚未被复用
1 🔴 红色底 + 复用 ×1 已复用一次,说明旧数据离开、新数据到来
N 🔴 红色底 + 复用 ×N 该实例已经被重用了 N 次
用户向下滚动列表时,可以看到每个卡片右上角的数字不断增长——复用 ×5、复用 ×8、复用 ×12——这些数字直观地证明了同一个组件实例正在被反复使用,而不是创建新的。

右上角还有一个微小的时间戳,显示最近一次 aboutToReuse 被调用的确切时刻(时:分:秒.毫秒),配合 DevEco Studio 的日志,可以精确定位复用的时间点。

  1. 普通模式 vs @Reusable 模式:对比实验
    7.1 切换机制
    页面顶部设计了一个模式切换栏,包含两个 Tab:

┌──────────────────────┬──────────────────────┐
│ ♻️ @Reusable 模式 │ ❌ 普通模式(对比) │
└──────────────────────┴──────────────────────┘
点击「@Reusable 模式」→ 蓝色高亮,列表使用 ReusableFeedItem(@Reusable 组件)
点击「普通模式」→ 灰色高亮,列表使用 NormalFeedItem(普通组件)
切换通过 @State useReusable 布尔值控制,build() 中的 if/else 分支分别渲染两种不同的 List:

if (this.useReusable) {
this.buildReusableList() // LazyForEach + ReusableFeedItem
} else {
this.buildNormalList() // LazyForEach + NormalFeedItem
}
7.2 对照组的 UI 差异
对照组 NormalFeedItem 在视觉上唯一的不同是右上角徽标显示「未复用」(灰色背景),而不是复用次数。这是为了让用户在切换时肉眼验证:普通模式下没有组件被复用,每个组件都是新建的。

7.3 用 DevEco Studio 验证复用效果
除了肉眼观察复用次数徽标,还可以用 DevEco Studio 的 ArkUI Inspector 工具来验证:

运行应用到 Previewer
打开 DevEco Studio → Tools → ArkUI Inspector
在 @Reusable 模式下快速滚动列表
观察 Inspector 中的组件树节点数
你会发现一个惊人的事实:不管滚动到哪里,组件树中的 ReusableFeedItem 节点数量始终维持在 10 个左右(等于可视区域容量),而不是 100 个。这就是 @Reusable 的魔力——100 条数据只用了 10 个组件实例。

7.4 对比实验的深层含义
这个对比实验回答了开发者在决定是否使用 @Reusable 时最常问的问题:

「我的列表需要 @Reusable 吗?」

判断标准很简单:

数据量 < 30 条,且不再增长 → 不需要,普通 ForEach 足够
数据量 30~100 条,结构简单 → 推荐使用,但普通 LazyForEach 也可接受
数据量 > 100 条,且可能继续增长 → 必须使用,否则会滑动卡顿
数据来自网络分页加载(无限列表)→ 必须使用,否则内存占用随加载次数线性增长
8. ArkTS 严格模式下的使用限制
在实现 @Reusable 组件的过程中,需要特别注意 ArkTS 严格模式与 TypeScript 的几个关键差异:

8.1 禁止对象字面量作为类型
// ❌ TypeScript 可以,ArkTS 不允许
const feedItem = (params as { item: FeedItem }).item;

// ✅ ArkTS 必须使用预定义的 interface
interface ReusableFeedItemParams {
item: FeedItem;
}
const feedItem: FeedItem = (params as ReusableFeedItemParams).item;
这个限制源自 ArkTS 的 arkts-no-obj-literals-as-types 规则——一切类型必须显式声明,不允许在表达式内创建匿名类型。

8.2 接口必须在 struct 外部声明
// ❌ 错误:接口声明在 struct 内部
@Reusable
@Component
struct MyComponent {
interface MyType { … } // arkts-no-global-decl
}

// ✅ 正确:在文件顶部声明
interface MyType { … }
@Reusable
@Component
struct MyComponent { … }
8.3 @Reusable 不能与 @Link/@Consume 同时使用
// ❌ 错误:@Reusable 组件不能使用 @Link
@Reusable
@Component
struct ReusableItem {
@Link data: FeedItem; // 编译错误或运行时异常
}

// ✅ 正确:使用 @Prop 或普通属性传递数据
@Reusable
@Component
struct ReusableItem {
@Prop item: FeedItem; // ✅ @Prop 可以
private item: FeedItem | null = null; // ✅ 普通属性也可以
}
原因:@Link 需要双向绑定到父组件的状态变量,而 @Reusable 组件的实例在回收池中时,其父组件可能已经消失,@Link 的绑定链会被破坏。

8.4 aboutToReuse 参数类型必须是 Object
// ✅ 正确
aboutToReuse(params: Object): void { }

// ❌ 错误(某些 SDK 版本不支持)
aboutToReuse(params: ReusableFeedItemParams): void { }
aboutToReuse 的回调签名是固定的——参数类型为 Object,需要在函数体内手动做类型转换。这是框架层面的约定,不可更改。

8.5 非空断言的使用
ArkTS 支持 ! 非空断言操作符,在已知某个值一定不为 null 时使用:

this.circle.fill(this.item!.avatarColor)
但请注意,不要滥用 !——如果运行时值为 null,非空断言不会阻止崩溃,只是绕过了编译器的类型检查。在不确定值的场景下,应使用条件判断(如 x?.prop ?? defaultValue)。

  1. 使用场景与最佳实践
    9.1 推荐使用 @Reusable 的场景
    场景 数据量级 推荐理由
    社交信息流(朋友圈/微博) 100~1000+ 结构复杂,滚动频繁
    电商商品列表 50~500+ 图片资源多,复用价值高
    聊天消息列表 100~10000+ 长度不限,消息格式统一
    文件/文件夹列表 50~500 每个文件项包含图标/名称/大小
    评论列表 30~300 回复嵌套结构可能导致更多子组件
    搜索结果列表 10~100+ 分页加载,每页都需要复用
    9.2 性能调优建议
    选择合适的复用池大小。框架的回收池大小不是手动配置的,而是由可视区域的容量自动决定。如果卡片高度很小(如 40vp),可视区域能容下 20+ 个组件;如果卡片高度很大(如 200vp),可能只能容下 5~6 个。可以通过调整卡片高度来间接控制复用池规模。

避免在 aboutToReuse 中做耗时操作。这个回调是在 UI 线程上同步执行的,如果耗时超过 5ms,就会导致列表滚动的帧率下降。常见的耗时操作包括:

aboutToReuse(params: Object): void {
// ✅ 快:赋值操作(纳秒级)
this.item = (params as ReusableFeedItemParams).item;

// ✅ 快:简单数值计算
this.reuseCount++;

// ❌ 慢:JSON 解析
this.data = JSON.parse(params.rawData); // 可能 1~10ms

// ❌ 慢:正则匹配
this.parsedContent = params.content.match(/…/g); // 可能 5~50ms

// ❌ 非常慢:网络请求(绝对禁止)
httpRequest(…); // 至少几十毫秒,会阻塞 UI
}
合理设置 keyGenerator。一个好的 key 应该:

使用数据项的业务 ID(而非数组下标)
稳定不变(数据更新时 ID 不变)
全局唯一(不同数据项不会重复)
// ✅ 好 key
(item: FeedItem) => item.id.toString()

// ❌ 坏 key
(item: FeedItem, index: number) => index.toString()
// 插入/删除操作会导致 key 错乱
9.3 与图片缓存的搭配
如果 @Reusable 组件中包含图片(Image 组件),建议配合图片缓存一起使用:

aboutToReuse(params: Object): void {
const feedItem = (params as ReusableFeedItemParams).item;
this.item = feedItem;

// 重置图片控制器(避免闪图:显示上一条数据的图片后突然跳到新图片)
if (this.imageController != null) {
this.imageController.reset();
}
}
9.4 结合分页加载
@Reusable 与分页加载是天生绝配。随着用户滚动加载更多数据,普通列表的组件实例数会线性增长,而 @Reusable 列表的组件实例数始终保持不变——因为回收池中的组件一直在被复用:

// 分页加载更多数据——即使数据从 100 加到 10000,
// @Reusable 列表的组件实例数始终维持在 ~10 个
loadMore(): void {
const newItems = fetchFromServer(/* page= */ ++this.page);
newItems.forEach(item => this.dataSource.addItem(item));
}
10. 完整源代码速查
10.1 数据模型与接口
// 数据模型
interface FeedItem {
id: number;
userName: string;
avatarColor: string;
content: string;
likes: number;
comments: number;
timeAgo: string;
}

// 复用参数接口(ArkTS 禁止对象字面量 as 类型转换,必须显式声明)
interface ReusableFeedItemParams {
item: FeedItem;
}
10.2 核心:@Reusable 组件
@Reusable
@Component
struct ReusableFeedItem {
private item: FeedItem | null = null;
@State private reuseCount: number = 0;
private createTime: string = ‘’;
@State private lastUpdateTime: string = ‘’;

aboutToAppear(): void {
if (this.createTime === ‘’) {
const now = new Date();
this.createTime = ${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}.${now.getMilliseconds()};
}
}

aboutToReuse(params: Object): void {
const feedItem = (params as ReusableFeedItemParams).item;
this.item = feedItem;
this.reuseCount++;
const now = new Date();
this.lastUpdateTime = ${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}.${now.getMilliseconds()};
}

build() {
Column() {
Row() {
Circle().width(36).height(36).fill(this.item?.avatarColor ?? ‘#FFCCCCCC’)
Column({ space: 2 }) {
Text(this.item?.userName ?? ‘—’).fontSize(14).fontWeight(FontWeight.Medium)
Text(this.item?.timeAgo ?? ‘’).fontSize(11).fontColor(‘#FF999999’)
}.layoutWeight(1).margin({ left: 10 })
// 复用次数徽标
Column() {
Text(‘复用 ×’ + this.reuseCount.toString()).fontSize(9).fontColor(‘#FFFFFF’)
}
.width(48).height(20)
.backgroundColor(this.reuseCount > 0 ? ‘#FFE74C3C’ : ‘#FF2ECC71’)
.borderRadius(10)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
}.width(‘100%’).alignItems(VerticalAlign.Center)

  Text(this.item?.content ?? '').fontSize(14).fontColor('#FF555555')
    .lineHeight(22).width('100%').margin({ top: 8, bottom: 6 })

  Row() {
    Text('❤ ' + (this.item?.likes.toString() ?? '0')).fontSize(12).fontColor('#FFE74C3C')
    Blank(6)
    Text('💬 ' + (this.item?.comments.toString() ?? '0')).fontSize(12).fontColor('#FF3498DB')
  }.width('100%').margin({ top: 4 })
}
.width('100%').padding(12).backgroundColor(Color.White).borderRadius(12)
.shadow({ radius: 3, color: '#1A000000', offsetY: 1 })

}
}
10.3 数据源 + 主页面
class FeedDataSource implements IDataSource {
private dataArray: FeedItem[] = [];
private listeners: DataChangeListener[] = [];
constructor(count: number) {
for (let i = 1; i <= count; i++) this.dataArray.push(this.generateItem(i));
}
totalCount(): number { return this.dataArray.length; }
getData(index: number): FeedItem { return this.dataArray[index]; }
registerDataChangeListener(listener: DataChangeListener): void {
this.listeners.push(listener);
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const idx = this.listeners.indexOf(listener);
if (idx !== -1) this.listeners.splice(idx, 1);
}
// 生成模拟数据…
}

@Entry
@Component
struct ReusableComponentDemo {
private dataSource: FeedDataSource = new FeedDataSource(100);
@State private useReusable: boolean = true;

build() {
Column() {
this.HeaderSection()
this.ModeSwitchBar()
if (this.useReusable) this.buildReusableList()
else this.buildNormalList()
}
.width(‘100%’).height(‘100%’).backgroundColor(‘#FFF5F5F5’)
}

@Builder
buildReusableList() {
List({ space: 10 }) {
LazyForEach(this.dataSource, (item: FeedItem) => {
ReusableFeedItem({ item: item })
}, (item: FeedItem) => item.id.toString())
}
.width(‘100%’).layoutWeight(1)
.edgeEffect(EdgeEffect.Spring)
}
}
11. 总结与延伸阅读
11.1 本文要点回顾
@Reusable 的核心原理:组件回收池机制——离开屏幕的组件不被销毁,进入回收池供新数据复用,实例总数 = 常数。

三个生命周期:

aboutToAppear():每次挂载时触发,适合做「每次出现都要执行」的初始化
aboutToReuse(params):灵魂方法,从 params 中取新数据并更新状态
aboutToDisappear():组件退场时触发,适合释放资源
最佳搭档 LazyForEach:按需创建、按需回收,是 @Reusable 发挥作用的前提。

IDataSource 数据源:实现 totalCount / getData / registerDataChangeListener / unregisterDataChangeListener 四个方法,支持动态增删。

ArkTS 严格模式限制:禁止对象字面量 as 类型转换、接口必须在 struct 外部、@Reusable 不能与 @Link 同时使用。

可视化验证:通过「复用 ×N」徽标和模式切换 Tab,直观展示组件复用的效果。

11.2 @Reusable 与 @Component 的生命周期差异
生命周期阶段 普通 @Component @Reusable 组件
首次创建 ❌ 无事件(直接初始化) aboutToAppear()
每次挂载到组件树 ❌ 不触发 aboutToAppear()(再次触发)
数据更新(复用) ❌ 无事件(需手动监听) aboutToReuse(params)
离开组件树 aboutToDisappear() aboutToDisappear()
销毁 自动销毁 进入回收池,不销毁
11.3 与其他布局方案的关系
本系列共有四篇文章,各自聚焦于不同的布局技术:

文章 聚焦点 核心技术 适用场景
本文 滚动性能优化 @Reusable + LazyForEach 大量数据列表、信息流
GridRow + breakpoints 响应式栅格 GridRow + columns + breakpoints 多设备自适应布局
GridRow + offset 栅格偏移对齐 offset + animateTo 表单布局、对齐控制
layoutWeight + animateTo 弹性权重分配 layoutWeight 弹窗、面板折叠
这些技术可以组合使用——例如用 GridRow 做列表页面的整体布局,用 @Reusable 优化列表内的每一行。

11.4 延伸阅读推荐
HarmonyOS 官方文档:@Reusable 装饰器 —— 最权威的 API 参考
HarmonyOS 官方文档:LazyForEach —— 懒加载迭代器的使用规范
HarmonyOS 官方文档:IDataSource 接口 —— 数据源协议完整定义
HarmonyOS 官方文档:组件生命周期 —— aboutToAppear / aboutToDisappear 完整说明
本文配套 Demo 项目路径:D:\HarmonyOS-Life\Demo0627 → 在 DevEco Studio 中打开 → 运行到 Previewer → 单击首页第四个按钮「♻️ 可复用组件(@Reusable)」进入演示页。点击顶部 Tab 切换 @Reusable 模式与普通模式,上下滚动列表观察卡片右上角复用计数的差异。

Logo

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

更多推荐