HarmonyOS应用开发实战:猫猫大作战-@Reusable 声明与复用池、复用触发条件、Reusable vs ForEach 取舍、与 Lazy

HarmonyOS应用开发实战:猫猫大作战-@Reusable 声明与复用池、复用触发条件、Reusable vs ForEach 取舍、与 Lazy 前言前面我们用ForEach渲染猫咪数组——每只猫一个CatItem子组件。但当列表项数多到几十甚至上百战绩历史、背包道具、关卡列表ForEach会全量渲染所有项哪怕屏幕只显示 5 个——性能浪费。HarmonyOS 提供了Reusable可复用组件装饰器——给子组件打上后列表滚动时离屏的项不销毁存入复用池新进入的项从池里取复用省去重建开销。本篇以「猫猫大作战」未来扩展的战绩历史列表为锚点把Reusable 声明与复用池、复用触发条件、Reusable vs ForEach 取舍、与 LazyForEach 搭配四大要点讲透。提示本系列不讲 ArkTS 基础语法与环境搭建假设你已跟完第 1–46 篇。本篇是阶段二第十七篇。一、场景拆解战绩历史长列表假设「猫猫大作战」要加战绩历史页——显示最近 100 局的得分/时间/最高连击State history: GameRecord[] []; // 100 条记录 List({ scroller: this.scroller }) { ForEach(this.history, (record: GameRecord) { ListItem() { RecordItem({ record: record }) } }, (record: GameRecord) record.id) } .cachedCount(5) // 预渲染 5 个痛点ForEach全量渲染 100 个RecordItem——屏幕只显示 8 个其余 92 个仍建组件树、占内存、耗 CPU。滚动时离屏项销毁、新项新建卡顿。Reusable 的解法Reusable Component export struct RecordItem { Prop record: GameRecord; build() { /* ... */ } } // List 检测到子组件是 Reusable自动启用复用池 List() { ForEach(this.history, (record: GameRecord) { ListItem() { RecordItem({ record: record }) } }, (record: GameRecord) record.id) } .cachedCount(5) **关键经验****Reusable 让 ListItem 离屏不销毁、入复用池新项从池取复用**——省重建开销滚动流畅。 ## 二、Reusable 声明与复用池 ### 2.1 给子组件打 Reusable ts Reusable Component export struct RecordItem { Prop record: GameRecord; build() { Row() { Text(this.record.score.toString()) .fontSize(16).fontWeight(FontWeight.Bold).fontColor(#2ECC71) Text(this.record.formatTime) .fontSize(14).fontColor(#7F8C8D) Text(x${this.record.maxCombo}) .fontSize(14).fontColor(#E74C3C) } .width(100%).height(56) .padding({ left: 16, right: 16 }) .border({ width: { bottom: 1 }, color: #ECF0F1 }) } }拆解片段含义Reusable装饰器标记「本组件可复用离屏入池」Component普通组件装饰器必须先有Prop record从父接收数据复用时重新传值关键约束Reusable 必须搭配 Component——单独 Reusable 无意义它是组件的复用标记。2.2 复用池机制List 滚动 ├─ 离屏项滚出可视区→ 不销毁存入复用池 └─ 新进入项滚入可视区→ 从池取复用重新传 Prop 值池的容量由cachedCount控制——预渲染 N 个离屏项 可视区项池容量约等于cachedCount 可视区项数。2.3 复用项的重新传值Reusable Component export struct RecordItem { Prop record: GameRecord; // 复用时重新传新 record build() { /* ... */ } } // 滚动时 // 1. 离屏的 RecordItemrecord历史第 3 局入池 // 2. 新进入的 RecordItem 从池取传 record历史第 13 局 // 3. 子组件 build() 重渲染显示第 13 局数据关键经验复用是「壳复用、数据重传」——组件实例不销毁但 Prop 重新赋值触发 build() 显示新数据。三、复用触发条件3.1 必须在 List/Grid 内// ✅ List 内触发复用 List() { ForEach(this.history, (record) { ListItem() { RecordItem({ record: record }) } }, (record) record.id) } .cachedCount(5) // ❌ 普通 Column 内不触发复用全量渲染 Column() { ForEach(this.history, (record) { RecordItem({ record: record }) }, (record) record.id) }机制只有List/Grid这类滚动容器有复用池逻辑——Column/Row是静态布局全量渲染。3.2 必须配合 ForEach 密钥List() { ForEach(this.history, (record: GameRecord) { ListItem() { RecordItem({ record: record }) } }, (record: GameRecord) record.id) // ← 密钥 }机制ForEach 用密钥做 diff配合 List 的复用池——密钥变触发 diffList 决定复用还是新建。3.3 cachedCount 预渲染List() { /* ... */ }.cachedCount(5)含义可视区外上下各预渲染 5 个——滚动时新项已就绪不闪白。cachedCount预渲染内存滚动流畅度00 个极低❌ 滚动闪白5推荐上下各 5中✅ 流畅10上下各 10高✅ 极流畅实战经验cachedCount(5)是通用平衡——内存适中滚动流畅。低端设备可降到 3。四、Reusable vs ForEach 取舍4.1 ForEach 全量渲染// 无 ReusableForEach 全量渲染所有项 ForEach(this.history, (record) { ListItem() { RecordItem({ record: record }) } }, (record) record.id) // 100 条记录 → 100 个 RecordItem 实例全占内存4.2 Reusable 池化渲染// 有 Reusable只渲染可视区 cachedCount Reusable Component export struct RecordItem { /* ... */ } List() { ForEach(this.history, (record) { ListItem() { RecordItem({ record: record }) } }, (record) record.id) }.cachedCount(5) // 100 条记录 → 只渲染可视区 8 个 cached 10 个 18 个实例4.3 对比表维度ForEach无 ReusableReusable实例数N全量可视区 cachedCount内存高N 个实例低18 个滚动销毁新建卡顿复用池流畅适合N 20少量N ≥ 20长列表必须在任意容器List/Grid关键经验N 20 用 ForEachN ≥ 20 用 Reusable List——少量列表全量渲染更简单长列表复用省内存。4.4 策策树列表项数 N ├─ N 20 → ForEach任意容器简单 └─ N ≥ 20 ├─ 静态不滚动 → ForEach全量也 OK └─ 滚动列表 → Reusable List cachedCount五、实战战绩历史列表用 Reusable5.1 定义 GameRecord 类型假设新建entry/src/main/ets/components/GameTypes.ets补充export interface GameRecord { id: string; // 唇一密钥 score: number; // 得分 formatTime: string; // 用时已格式化 maxCombo: number; // 最高连击 mergeCount: number; // 合并次数 highestLevel: CatLevel; // 最高等级 date: string; // 日期 }5.2 创建 RecordItem 子组件新建entry/src/main/ets/components/RecordItem.etsimport { GameRecord } from ./GameTypes; Reusable // ← 本篇重点标记可复用 Component export struct RecordItem { Prop record: GameRecord; build() { Row() { // 得分 Column() { Text(得分).fontSize(10).fontColor(#95A5A6) Text(this.record.score.toString()) .fontSize(18).fontWeight(FontWeight.Bold).fontColor(#2ECC71) } .alignItems(HorizontalAlign.Start) .layoutWeight(2) // 用时 Column() { Text(用时).fontSize(10).fontColor(#95A5A6) Text(this.record.formatTime) .fontSize(16).fontWeight(FontWeight.Medium).fontColor(#2C3E50) } .alignItems(HorizontalAlign.Center) .layoutWeight(2) // 最高连击 Column() { Text(连击).fontSize(10).fontColor(#95A5A6) Text(x${this.record.maxCombo}) .fontSize(16).fontWeight(FontWeight.Bold).fontColor(#E74C3C) } .alignItems(HorizontalAlign.End) .layoutWeight(2) } .width(100%).height(64) .padding({ left: 16, right: 16, top: 8, bottom: 8 }) .border({ width: { bottom: 1 }, color: #ECF0F1 }) .onClick(() { // 点击查看详情第 71 篇会专讲导航 console.info(click record, this.record.id); }) } }5.3 创建战绩历史页新建entry/src/main/ets/pages/HistoryPage.etsimport { GameRecord } from ../components/GameTypes; import { RecordItem } from ../components/RecordItem; Component export struct HistoryPage { State history: GameRecord[] []; private scroller: Scroller new Scroller(); aboutToAppear() { // 加载历史记录第 145 篇会专讲持久化 this.history this.loadHistory(); } loadHistory(): GameRecord[] { // 简化模拟 100 条记录 const records: GameRecord[] []; for (let i 0; i 100; i) { records.push({ id: record_${i}, score: 500 Math.floor(Math.random() * 2000), formatTime: ${Math.floor(Math.random() * 10)}:${Math.floor(Math.random() * 60).toString().padStart(2, 0)}, maxCombo: Math.floor(Math.random() * 10) 1, mergeCount: Math.floor(Math.random() * 50) 10, highestLevel: CatLevel.SMALL, date: 2026-07-${(20 - Math.floor(i / 10)).toString().padStart(2, 0)} }); } return records; } build() { Column() { // 标题栏 Row() { Text(战绩历史).fontSize(20).fontWeight(FontWeight.Bold).fontColor(#2C3E50) } .width(100%).height(56) .padding({ left: 16, right: 16 }) .justifyContent(FlexAlign.Start).alignItems(VerticalAlign.Center) // 战绩列表Reusable List本篇重点 List({ scroller: this.scroller }) { ForEach(this.history, (record: GameRecord) { ListItem() { RecordItem({ record: record }) // ← Reusable 子组件 } }, (record: GameRecord) record.id) } .width(100%) .layoutWeight(1) .cachedCount(5) // ← 预渲染 5 个 .divider({ strokeWidth: 1, color: #ECF0F1, startMargin: 16, endMargin: 16 }) } .width(100%).height(100%) .backgroundColor(#FFFFFF) } }5.4 改造对比维度ForEach 全量版Reusable 版100 条实例数100 个18 个可视 8 cached 10内存~100 × RecordItem~18 × RecordItem滚动流畅卡顿销毁新建流畅复用池代码量少无装饰器略多Reusable适合N 20N ≥ 20六、Reusable 与 LazyForEach 搭配6.1 LazyForEach 按需渲染Reusable解决「复用组件实例」LazyForEach解决「按需调用数据源」——两者搭配是长列表最优解import { LazyForEach, IDataSource } from kit.ArkUI; // 数据源实现 IDataSource class HistoryDataSource implements IDataSource { private records: GameRecord[] []; totalCount(): number { return this.records.length; } getData(index: number): GameRecord { return this.records[index]; } registerDataChangeListener(listener: DataChangeListener): void { /* ... */ } unregisterDataChangeListener(listener: DataChangeListener): void { /* ... */ } reloadData(records: GameRecord[]): void { this.records records; // 通知监听器数据变 } } // 使用 private dataSource: HistoryDataSource new HistoryDataSource(); List({ scroller: this.scroller }) { LazyForEach(this.dataSource, (record: GameRecord) { ListItem() { RecordItem({ record: record }) // Reusable 子组件 } }, (record: GameRecord) record.id) } .cachedCount(5)机制LazyForEach只对可视区 cachedCount 范围的索引调getData不全量遍历 100 条。Reusable可视区外的 ListItem 入复用池新进入的复用壳。关键经验LazyForEach Reusable 是长列表最优解——前者省数据遍历后者省组件实例。本系列第 48 篇会专讲 LazyForEach。6.2 三种列表方案对比方案数据遍历组件实例适合ForEach全量 N 次全量 N 个N 20Reusable ForEach全量 N 次可视区 cachedN 20-100数据简单LazyForEach Reusable按需 18 次可视区 cachedN ≥ 100数据复杂七、踩坑提示7.1 Reusable 用在非 List/Grid// ❌ 错误Column 内用 Reusable无复用池逻辑退化为全量 Reusable Component export struct RecordItem { /* ... */ } Column() { ForEach(this.history, (record) { RecordItem({ record: record }) }, (record) record.id) } // 还是全量渲染 100 个 // ✅ 正确List 内用 List() { ForEach(this.history, (record) { ListItem() { RecordItem({ record: record }) } }, (record) record.id) }.cachedCount(5)7.2 忘 cachedCount// ❌ 错误没 cachedCount滚动时新项未预渲染闪白 List() { /* Reusable */ } // ✅ 正确设 cachedCount List() { /* Reusable */ }.cachedCount(5)7.3 Reusable 子组件用 Link// ⚠️ 复用时 Link 会重新绑定可能引起父 state 错乱 Reusable Component export struct RecordItem { Link record: GameRecord; // ❌ 不推荐 Link build() { /* ... */ } } // ✅ 推荐Reusable 子组件用 Prop只读复用时重传新值 Reusable Component export struct RecordItem { Prop record: GameRecord; // 只读复用安全 build() { /* ... */ } }关键经验Reusable 子组件用 Prop 不用 Link——复用时只读数据重传避免双向绑定错乱。7.4 ForEach 密钥不唯一// ❌ 错误密钥不唯一复用池错乱 ForEach(this.history, (record) { ListItem() { RecordItem({ record: record }) } }, (record) record.score.toString()) // 多条同得分密钥冲突 // ✅ 正确密钥唯一 ForEach(this.history, (record) { ListItem() { RecordItem({ record: record }) } }, (record) record.id) // id 唯一八、调试技巧DevEco Profiler 看组件实例数滚动时实例数应稳定在可视区cached不应随 N 增长。console.info在子组件 aboutToReuse追复用时机如果有生命周期。滚动卡顿排查检查是否在 List 内检查 cachedCount 是否设检查是否用了 LazyForEach。内存对比DevEco Memory Profiler 看 RecordItem 实例数Reusable 应远少于 N。九、性能与最佳实践N ≥ 20 的滚动列表用 Reusable——省组件实例滚动流畅。Reusable 必须在 List/Grid 内——普通容器无复用池逻辑。cachedCount(5) 平衡内存与流畅——低端设备可降到 3。Reusable 子组件用 Prop 不用 Link——复用时只读数据重传避免双向错乱。密钥必须唯一——ForEach diff 和复用池都靠密钥冲突错乱。N ≥ 100 用 LazyForEach Reusable——前者省数据遍历后者省组件实例。总结本篇我们从 Reusable 列表复用切入掌握了**Reusable 声明与复用池机制**、复用触发条件List/Grid ForEach 密钥 cachedCount、Reusable vs ForEach 取舍N 20 ForEachN ≥ 20 Reusable、与 LazyForEach 搭配四大要点并给出了战绩历史列表用 Reusable 的完整代码。核心要点Reusable 在 List 内离屏入池复用cachedCount(5) 平衡子用 Prop 不用 LinkN ≥ 100 搭 LazyForEach。下一篇我们将专讲 LazyForEach——大列表按需渲染。如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力相关资源「猫猫大作战」项目源码本仓库entry/src/main/ets/components/、entry/src/main/ets/pages/ArkUI Reusable 可复用组件官方指南List 列表组件官方指南ForEach 循环渲染官方指南ArkUI 长列表性能最佳实践开源鸿蒙跨平台社区HarmonyOS 开发者官方文档首页系列索引本仓库articles/INDEX.md