【共创季稿事节】鸿蒙 ArkTS 布局进阶:@Reusable 可复用组件 —— 列表滚动性能优化的终极武器

发布时间:2026/6/29 17:18:45
【共创季稿事节】鸿蒙 ArkTS 布局进阶:@Reusable 可复用组件 —— 列表滚动性能优化的终极武器 目录写在前面列表性能瓶颈从何而来Reusable 的核心原理三个生命周期方法详解aboutToAppear组件登场aboutToReuse灵魂方法——数据更新aboutToDisappear组件退场LazyForEachReusable 的最佳搭档IDataSource 数据源协议实战拆解Feed 信息流列表项目结构一览数据模型设计Reusable 组件完整实现复用计数可视化设计普通模式 vs Reusable 模式对比实验ArkTS 严格模式下的使用限制使用场景与最佳实践完整源代码速查总结与延伸阅读写在前面列表性能瓶颈从何而来1.1 一个数字的震撼想象一个社交应用的信息流页面100 条动态每条动态包含头像、用户名、正文、点赞按钮、评论按钮等十几个子组件。如果使用普通的 ForEach 或 LazyForEach 非可复用组件当用户从第一条滚动到最后一条时框架会创建 100 个列表项组件 × 每个项 ~15 个子组件 1500 次组件创建销毁 约 80 个离开屏幕的组件 × ~15 个子组件 1200 次组件销毁总操作2700 次组件树变更如果用户反复上下滚动这个数字还会成倍增长。在低端设备上这种频繁的创建/销毁直接表现为 滚动卡顿、掉帧、甚至白屏。1.2 传统方案的局限在 Reusable 出现之前开发者通常会采取以下措施来缓解列表性能问题方案 原理 问题虚拟滚动Virtual Scroll 只渲染可视区域内的节点 每个节点仍会被反复创建销毁减少子组件层级 扁平化组件树 牺牲了代码的可读性和模块化图片懒加载 只加载可视区域内的图片 治标不治本组件本身的开销还在分页加载 减少单次数据量 交互不连续用户体验下降这些方案要么治标不治本要么需要开发者手动实现复杂的回收逻辑。Reusable 的出现彻底改变了这一局面——框架内置了组件回收池开发者只需一个 Reusable 装饰器和两个生命周期回调就能将列表性能提升到接近理论极限。Reusable 的核心原理2.1 什么是 ReusableReusable 是 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 为例N100, 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 左右。三个生命周期方法详解Reusable 组件比普通 Component 多了两个生命周期回调加上原有的 aboutToAppear构成了完整的「创建—复用—销毁」生命周期链。3.1 aboutToAppear组件登场调用时机组件首次创建并挂载到组件树时组件从回收池取出并重新挂载时与普通组件的关键区别// 普通 ComponentaboutToAppear() // 只会调用一次组件创建时// Reusable ComponentaboutToAppear() // 每次重新挂载都会调用包括复用后这意味着「只需要执行一次」的逻辑如注册全局事件、初始化定时器需要用标志位控制不要在 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;// ② 更新组件内部数据变量渲染时会根据新数据更新 UIthis.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. LazyForEachReusable 的最佳搭档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 变量// ✅ 正确写法属性传参框架将参数传给 aboutToReuseLazyForEach(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数组下标因为插入/删除操作会导致下标错乱引起复用异常。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 从数组中移除指定 listener5.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 移动到 toonDataUpdated(index) — 指定索引的数据被更新选择最精确的通知方法可以让框架只更新受影响的部分而不是重建整个列表进一步提升性能。实战拆解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 的 keyuserName: 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 组件完整实现ReusableComponentstruct 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 的日志可以精确定位复用的时间点。普通模式 vs Reusable 模式对比实验7.1 切换机制页面顶部设计了一个模式切换栏包含两个 Tab┌──────────────────────┬──────────────────────┐│ ♻️ Reusable 模式 │ ❌ 普通模式对比 │└──────────────────────┴──────────────────────┘点击「Reusable 模式」→ 蓝色高亮列表使用 ReusableFeedItemReusable 组件点击「普通模式」→ 灰色高亮列表使用 NormalFeedItem普通组件切换通过 State useReusable 布尔值控制build() 中的 if/else 分支分别渲染两种不同的 Listif (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 必须使用预定义的 interfaceinterface ReusableFeedItemParams {item: FeedItem;}const feedItem: FeedItem (params as ReusableFeedItemParams).item;这个限制源自 ArkTS 的 arkts-no-obj-literals-as-types 规则——一切类型必须显式声明不允许在表达式内创建匿名类型。8.2 接口必须在 struct 外部声明// ❌ 错误接口声明在 struct 内部ReusableComponentstruct MyComponent {interface MyType { … } // arkts-no-global-decl}// ✅ 正确在文件顶部声明interface MyType { … }ReusableComponentstruct MyComponent { … }8.3 Reusable 不能与 Link/Consume 同时使用// ❌ 错误Reusable 组件不能使用 LinkReusableComponentstruct ReusableItem {Link data: FeedItem; // 编译错误或运行时异常}// ✅ 正确使用 Prop 或普通属性传递数据ReusableComponentstruct 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。使用场景与最佳实践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 组件ReusableComponentstruct 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);}// 生成模拟数据…}EntryComponentstruct 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’)}BuilderbuildReusableList() {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 模式与普通模式上下滚动列表观察卡片右上角复用计数的差异。