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

📅 2026/7/28 2:32:33 👁️ 阅读次数 📝 编程学习
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. 离屏的 RecordItem(record=历史第 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 的复用池——密钥变触发 diff,List 决定复用还是新建。

3.3 cachedCount 预渲染

List() { /* ... */ }.cachedCount(5)

含义:可视区外上下各预渲染 5 个——滚动时新项已就绪,不闪白。

cachedCount预渲染内存滚动流畅度
00 个极低❌ 滚动闪白
5(推荐)上下各 5✅ 流畅
10上下各 10✅ 极流畅

实战经验cachedCount(5)是通用平衡——内存适中,滚动流畅。低端设备可降到 3。

四、@Reusable vs ForEach 取舍

4.1 ForEach 全量渲染

// 无 @Reusable:ForEach 全量渲染所有项 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(无 Reusable)@Reusable
实例数N(全量)可视区 + cachedCount
内存高(N 个实例)低(18 个)
滚动销毁新建卡顿复用池流畅
适合N < 20(少量)N ≥ 20(长列表)
必须在任意容器List/Grid

关键经验N < 20 用 ForEach,N ≥ 20 用 @Reusable + List——少量列表全量渲染更简单,长列表复用省内存。

4.4 策策树

列表项数 N? ├─ N < 20 → ForEach(任意容器,简单) └─ N ≥ 20 ├─ 静态不滚动 → ForEach(全量也 OK) └─ 滚动列表 → @Reusable + List + cachedCount

五、实战:战绩历史列表用 @Reusable

5.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.ets

import { 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.ets

import { 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 < 20
@Reusable + 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 唯一

八、调试技巧

  1. DevEco Profiler 看组件实例数:滚动时实例数应稳定在可视区+cached,不应随 N 增长。
  2. console.info在子组件 aboutToReuse:追复用时机(如果有生命周期)。
  3. 滚动卡顿排查:检查是否在 List 内;检查 cachedCount 是否设;检查是否用了 LazyForEach。
  4. 内存对比:DevEco Memory Profiler 看 RecordItem 实例数,@Reusable 应远少于 N。

九、性能与最佳实践

  1. N ≥ 20 的滚动列表用 @Reusable——省组件实例,滚动流畅。
  2. @Reusable 必须在 List/Grid 内——普通容器无复用池逻辑。
  3. cachedCount(5) 平衡内存与流畅——低端设备可降到 3。
  4. @Reusable 子组件用 @Prop 不用 @Link——复用时只读数据重传,避免双向错乱。
  5. 密钥必须唯一——ForEach diff 和复用池都靠密钥,冲突错乱。
  6. N ≥ 100 用 LazyForEach + @Reusable——前者省数据遍历,后者省组件实例。

总结

本篇我们从 @Reusable 列表复用切入,掌握了**@Reusable 声明与复用池机制**、复用触发条件(List/Grid + ForEach 密钥 + cachedCount)@Reusable vs ForEach 取舍(N < 20 ForEach,N ≥ 20 Reusable)与 LazyForEach 搭配四大要点,并给出了战绩历史列表用 @Reusable 的完整代码。核心要点:@Reusable 在 List 内离屏入池复用;cachedCount(5) 平衡;子用 @Prop 不用 @Link;N ≥ 100 搭 LazyForEach

下一篇我们将专讲 LazyForEach——大列表按需渲染。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

  • 「猫猫大作战」项目源码:本仓库entry/src/main/ets/components/entry/src/main/ets/pages/
  • ArkUI @Reusable 可复用组件官方指南
  • List 列表组件官方指南
  • ForEach 循环渲染官方指南
  • ArkUI 长列表性能最佳实践
  • 开源鸿蒙跨平台社区
  • HarmonyOS 开发者官方文档首页
  • 系列索引:本仓库articles/INDEX.md