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

相关新闻

AI系统提示词精简优化:提升模型响应效果的关键策略

AI系统提示词精简优化:提升模型响应效果的关键策略

在AI模型开发和应用过程中,系统提示词的设计质量直接影响着模型的响应效果和用户体验。很多开发者习惯在系统提示词中加入大量约束条件和详细说明,试图精确控制模型行为,但过度复杂的提示词反而会干扰模型的核心能力发挥。本文将深入探讨系统…

2026/9/25 18:49:22 阅读更多 →
基于Eclipse Milo的OPC UA服务器快速搭建与工业数据采集实践

基于Eclipse Milo的OPC UA服务器快速搭建与工业数据采集实践

那天下午,我正和一位做工业自动化集成的朋友聊天。他刚接了个新项目,客户要求把产线上十几台不同品牌、不同年代的设备数据实时采集上来,统一展示。他第一反应是找硬件网关,但算下来成本直接飙到五位数,工期还拖了一个…

2026/10/9 12:24:42 阅读更多 →
Android平台OPC UA服务器开发:Eclipse Milo实现与工业自动化应用

Android平台OPC UA服务器开发:Eclipse Milo实现与工业自动化应用

在工业自动化领域,OPC(OLE for Process Control)技术扮演着至关重要的角色,它解决了不同厂商设备和软件系统之间的数据交换难题。对于初创团队而言,从项目第一天就实现盈利是一个极具挑战性的目标,而OPC技术…

2026/10/10 0:47:13 阅读更多 →

最新新闻

RK3588上MobileNet部署:推理链路、量化精度与实时识别优化

RK3588上MobileNet部署:推理链路、量化精度与实时识别优化

上一讲我们一路从装 SDK、配环境,到把 MobileNet 的 ONNX 模型成功转成 RKNN 格式,不少读者留言说终于走到了.rknn这一步。但我得先泼盆冷水:拿到.rknn文件只是走完了一半,真正的嵌入式 AI 部署战场是推理链路、预处理、量化精度和…

2026/10/11 14:47:44 阅读更多 →
Commands、Skills还是Agents?详解Context Engineering Kit的Token高效架构

Commands、Skills还是Agents?详解Context Engineering Kit的Token高效架构

AI 技能/插件提示工程AI 评测人工智能 【免费下载链接】context-engineering-kit Hand-crafted Claude Code Skills focused on improving agent results quality. Compatible with OpenCode, Cursor, Antigravity, Gemini CLI, and others. Includes CodeRabbit open-source a…

2026/10/11 14:47:44 阅读更多 →
基于Java的酒店管理系统设计与可视化实战解析

基于Java的酒店管理系统设计与可视化实战解析

这两天收到不少同学私信,都在问Java方向的毕设到底怎么选题、怎么落地。我手头正好刚整理完一套《基于Java的酒店管理系统设计与可视化》的毕设源码,编号47036,从功能设计到前端展示再到数据可视化都给你串好了,索性写一篇完整的拆…

2026/10/11 14:47:44 阅读更多 →
【开发心得】大模型背后的“隐形毒药”

【开发心得】大模型背后的“隐形毒药”

目录 ​编辑 1. 引言:从一则新闻说起 2. 什么是数据投毒 3. 数据投毒的技术原理 4. 数据投毒的主要类型 5. 数据投毒的危害 6. 如何防御数据投毒 7. 结语 1. 引言:从一则新闻说起 近期,有媒体报道称,日本右翼势力试图通过…

2026/10/11 14:47:44 阅读更多 →
成都国际学校哪家好:青白江为明学校的科技特色与培养路径参考

成都国际学校哪家好:青白江为明学校的科技特色与培养路径参考

成都国际学校了解指南:青白江为明学校的科技特色与培养路径参考 在成都,民办学校的选择一直是家长群体中关注度较高的话题。随着教育需求日趋多元化,越来越多的家庭开始关注学校的课程特色、师资配置以及与孩子学段的适配程度。对于成都青白江…

2026/10/11 14:47:44 阅读更多 →
学生学籍管理系统数据库课程设计:从ER图到MySQL事务与索引实践

学生学籍管理系统数据库课程设计:从ER图到MySQL事务与索引实践

简介:面向数据库课程设计学生,这份PDF完整呈现了学生学籍管理系统的开发全过程,针对传统手工学籍管理效率低、数据易丢失、统计易出错等痛点,给出了一套计算机化、可共享数据的解决方案。资源仅含1个PDF文件,压缩包858…

2026/10/11 14:46:44 阅读更多 →

日新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 10:45:37 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:53 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:54 阅读更多 →