鸿蒙应用开发UI基础第三十节:循环渲染核心ForEach 实战与性能优化
本文系统介绍了鸿蒙ArkTS框架中ForEach循环渲染的核心机制与最佳实践。主要内容包括:ForEach的基础语法与键值生成规则,强调键值唯一性对组件复用和性能优化的关键作用;详细解析首次渲染与非首次渲染时的差异处理逻辑,通过代码示例演示数据源修改对组件创建/销毁的影响;针对常见问题(键值重复、性能损耗)提供解决方案,建议使用稳定键值而非默认索引;最后给出工程目录结构说明和自定义组件实现示例。掌
【学习目标】
- 掌握 ForEach 循环渲染核心语法、键值生成规则与组件创建逻辑;
- 理解 ForEach 首次渲染与非首次渲染的差异,避免渲染异常;
- 规避 ForEach 常见错误(键值重复、性能损耗、数据不渲染);
- 掌握 ForEach 性能优化技巧,实现高效循环渲染。
一、本节工程目录结构
ForEachDemo/
├── entry/
│ └── src/main/
│ ├── ets/
│ │ ├── components/
│ │ │ ├── CustomListItem.ets # 自定义列表组件
│ │ ├── pages/
│ │ │ ├── Index.ets # 首页:功能导航入口
│ │ │ ├── BasicRenderDemo.ets # 基础渲染与键值示例
│ │ │ └── SkeletonListDemo.ets # 静态列表/骨架屏示例
│ └── resources/
│ └── base/media/ # 图片资源
└── build-profile.json5
二、ForEach 核心认知
2.1 什么是 ForEach?
ForEach 是鸿蒙 ArkTS 提供的循环渲染接口,基于数组数据源批量生成组件,需与容器组件配合使用,核心价值是简化重复组件的开发,提升代码复用性。
2.2 核心语法
ForEach(
dataSource: Array<T>, // 数据源(数组类型)
itemGenerator: (item: T, index?: number) => void, // 组件生成函数(遍历数组项创建组件)
keyGenerator?: (item: T, index?: number) => string // 键值生成函数(可选,默认按 index+JSON.stringify(item) 生成)
)
2.3 键值生成规则
键值(Key)是 ForEach 识别组件唯一性的核心,用于判断组件是否需要创建/复用/销毁:
- 若自定义
keyGenerator:需返回唯一且持久的字符串(如对象的唯一 ID); - 若未自定义:框架默认生成规则为
index + '__' + JSON.stringify(item); - 键值重复后果:框架仅创建第一个重复键值对应的组件,后续重复项不渲染;
- 键值变化后果:框架视为组件替换,销毁旧组件并创建新组件。
三、ForEach 渲染机制详解
3.1 首次渲染
遍历数据源,为每个数组项生成唯一键值,根据键值创建对应组件并渲染,流程:
- 遍历数组每一项;
- 调用
keyGenerator生成键值; - 若键值未存在,创建组件并记录键值;
- 若键值已存在,跳过组件创建(避免重复渲染)。
自定义列表项
// 子组件:自定义列表项
@Component
export struct CustomListItem {
@Prop content: string;
aboutToAppear(): void {
console.log(`CustomListItem - 组件即将出现 ---- ${this.content}`)
}
aboutToDisappear(): void {
console.log(`CustomListItem - 组件即将消失 ---- ${this.content}`)
}
build() {
Text(this.content)
.fontSize(18)
.padding(15)
.backgroundColor('#F7F8FA')
.borderRadius(8)
.width('100%')
}
}
ForEach基础渲染示例
import { CustomListItem } from '../components/CustomListItem';
@Entry
@Component
struct BasicRenderDemo {
@State fruitList: string[] = ['苹果', '香蕉', '橙子', '葡萄'];
build() {
Column({ space: 12 }) {
Text('ForEach核心细节')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 10 })
// ForEach 首次渲染:无重复项
ForEach(
this.fruitList,
(item:string) => CustomListItem({ content: item }), // 生成组件
(item:string) => item // 键值为数组项本身(确保唯一)
)
}
.padding(20)
.width('100%')
.height('100%')
.backgroundColor($r('sys.color.point_color_checked'))
}
}
首次渲染—无重复项

修改数据源
// 数据源包含重复项
@State fruitList: string[] = ['苹果', '香蕉', '香蕉', '葡萄'];
首次渲染—有重复项
仅渲染「苹果、香蕉、葡萄」三项,重复的「香蕉」因键值重复被跳过。
3.2 非首次渲染(数据源变化后)
当数据源发生变化(增删改查)时,框架触发重新渲染,流程:
- 遍历新数据源,生成新键值列表;
- 对比新旧键值列表:
- 新键值不存在:创建新组件;
- 新键值已存在:复用旧组件(不重新创建);
- 旧键值不存在于新列表:销毁对应组件。
修改示例
@Entry
@Component
struct BasicRenderDemo {
@State fruitList: string[] = ['苹果', '香蕉', '香蕉', '葡萄'];
// 修改第三项值
updateThirdItem() {
this.fruitList[2] = '芒果';
}
// 插入第一行
insertFirstItem() {
this.fruitList.splice(0, 0, '车厘子');
}
build() {
Column({ space: 12 }) {
Text('ForEach核心细节')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 10 })
Button('修改第三项为芒果')
.width('100%')
.backgroundColor($r('sys.color.brand'))
.fontColor(Color.White)
.onClick(() => this.updateThirdItem())
Button('新增插入第一行车厘子')
.width('100%')
.backgroundColor($r('sys.color.brand'))
.fontColor(Color.White)
.onClick(() => this.insertFirstItem())
ForEach(
this.fruitList,
(item:string) => CustomListItem({ content: item }), // 生成组件
(item:string) => item // 键值为数组项本身(确保唯一)
)
}
.padding(20)
.width('100%')
.height('100%')
.backgroundColor($r('sys.color.point_color_checked'))
}
}
点击修改第三项为芒果按钮后:原本重复项香蕉只创建了一个,修改后键值改变无重复项,创建了芒果,其他组件复用。
CustomListItem - 组件即将出现 ----苹果
CustomListItem - 组件即将出现 ----香蕉
CustomListItem - 组件即将出现 ----芒果
CustomListItem - 组件即将出现 ----葡萄
点击插入第一行车厘子按钮后:新创建了车厘子,其他组件复用。
CustomListItem - 组件即将出现 ---- 车厘子
修改键值使用系统默认 index + '__' + JSON.stringify(item)
当使用 index 进行新增、删除、修改数据源后,键值会变动,造成组件重复销毁与创建,产生性能损耗。
ForEach(
this.fruitList,
(item:string) => CustomListItem({ content: item }), // 生成组件
// (item:string) => item // 键值为数组项本身(确保唯一)
)
首次运行日志输出:
组件全部显示
CustomListItem - 组件即将出现 ---- 苹果
CustomListItem - 组件即将出现 ---- 香蕉
CustomListItem - 组件即将出现 ---- 香蕉
CustomListItem - 组件即将出现 ---- 葡萄
点击插入第一行车厘子按钮后:数组存储 ["车厘子","苹果","香蕉","香蕉","葡萄"],键值改变为 车厘子_0 苹果_1 香蕉_2 香蕉_3 葡萄_4,而原键值为 苹果_0 香蕉_1 香蕉_2 葡萄_3。此时 香蕉_2 无变动,其他组件键值已变化,日志输出如下:
CustomListItem - 组件即将消失 ---- 苹果
CustomListItem - 组件即将消失 ---- 葡萄
CustomListItem - 组件即将消失 ---- 香蕉
CustomListItem - 组件即将出现 ---- 车厘子
CustomListItem - 组件即将出现 ---- 苹果
CustomListItem - 组件即将出现 ---- 香蕉
CustomListItem - 组件即将出现 ---- 葡萄
示例 List + ForEach
import { CustomListItem } from '../components/CustomListItem';
@Entry
@Component
struct ListRenderDemo {
@State fruitList: string[] = [];
aboutToAppear(): void {
for (let index = 0; index < 100; index++) {
this.fruitList.push(`组件${index}`)
}
}
// 修改第三项值
updateThirdItem() {
this.fruitList[2] = '芒果';
}
// 插入第一行
insertFirstItem() {
this.fruitList.splice(0, 0, '车厘子');
console.log(JSON.stringify( this.fruitList))
}
build() {
Column({ space: 12 }) {
Text('List + ForEach核心细节')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 10 })
Button('修改第三项为芒果')
.width('100%')
.backgroundColor($r('sys.color.brand'))
.fontColor(Color.White)
.onClick(() => this.updateThirdItem())
Button('新增插入第一行车厘子')
.width('100%')
.backgroundColor($r('sys.color.brand'))
.fontColor(Color.White)
.onClick(() => this.insertFirstItem())
this.scrollerBuilder()
// this.listBuilder()
}
.padding(20)
.width('100%')
.height('100%')
.backgroundColor($r('sys.color.point_color_checked'))
}
@LocalBuilder scrollerBuilder(){
Scroll(){
Column(){
ForEach(this.fruitList, (item:string) =>{
CustomListItem({ content: item }) // 生成组件
}, (item:string) => item)
}
}
.width('100%')
.layoutWeight(1)
}
@LocalBuilder listBuilder(){
List(){
ForEach(this.fruitList, (item:string) =>{
ListItem(){
CustomListItem({ content: item }) // 生成组件
}
}, (item:string) => item)
}.width('100%')
.layoutWeight(1)
}
}
scrollerBuilder 首次运行
CustomListItem - 组件即将出现 ----组件0
... 连续输出
CustomListItem - 组件即将出现 ----组件99

检查视图
组件从0创建到99,总计100条,可视区域仅10条,不可视区域也同样创建并挂载到组件树上。如果是1000条数据,ForEach依然会全部创建,渲染性能极低,内存消耗极大。

使用List+ForEach(listBuilder)
// this.scrollerBuilder()
this.listBuilder()
再次运行查看日志
配合List组件后,循环渲染性能有所提升。从0创建到23,并未全部创建。页面可视区域显示11条(0-10),不可见区域预创建13条。

检查UI视图
我们发现List+ForEach循环渲染组件性能大幅提升。虽然首次运行创建了23个组件,可视区域显示10条,但不可见区域的13条并未直接挂载到组件树上。
点击修改第三项为芒果
销毁原来的组件2,新创建芒果,其他组件复用。
CustomListItem - 组件即将消失 ---- 组件2
CustomListItem - 组件即将出现 ----芒果
滑动列表查看日志
向上滑动屏幕直到组件10消失,组件11成为可视区域第一行。日志开始继续输出,从第24条至34条停止。由此可见,List会提前预创建组件,在即将显示时挂载到组件树上。
CustomListItem - 组件即将出现 ---- 组件24
CustomListItem - 组件即将出现 ---- 组件25
.....
CustomListItem - 组件即将出现 ---- 组件33
CustomListItem - 组件即将出现 ---- 组件34
- 通过以上演示,List虽然提升了加载性能,但使用ForEach修改
@State状态变量时,仍会销毁并重新创建组件。 - 使用
index作为键值,数据源变化后键值会重新排序,销毁@State之后的所有组件并重新创建。因此ForEach仅适合静态少量列表、数据源不变的场景。 - 注意:使用JSON.stringify(item)作为键值时,数据量不宜过大内存会飙升;若数据中包含bigint类型,JSON.stringify会直接抛出错误,导致程序无法正常运行。
四、ForEach适用场景
适用于静态无变动的数据源列表、数据量较小(通常少于100条)。例如骨架屏、固定选项列表、设置页等场景。
示例:骨架屏静态渲染
@Entry
@Component
struct SkeletonListDemo {
// 静态数据源:仅用于控制骨架屏数量
@State skeletonCount: number[] = [1, 2, 3, 4, 5];
@State translateX: string = '-100%'; // 动画偏移量
@State duration: number = 1500; // 动画速度
aboutToAppear(): void {
// 页面渲染后自动开始流光动画
this.startShineAnimation();
}
// 流光动画(无限循环)
startShineAnimation() {
// 延迟100ms:等待组件渲染完成,否则动画会因组件未挂载而不生效
setTimeout(() => {
this.translateX = '-100%'; // 设置起始位置
this.getUIContext()?.animateTo({
duration: this.duration,
iterations: -1, // 无限循环
curve: Curve.Linear
}, () => {// 动画事件回调 让渐变组件移动'100%'
this.translateX = '100%';
});
}, 100)
}
// 流光遮罩层
@LocalBuilder shineOverlay(){
Row()
.width('100%')
.height('100%')
.translate({ x: this.translateX })
.linearGradient({ // 线性渐变
angle: 90,
colors: [
['rgba(255,255,255,0)', 0],
['rgba(255,255,255,0.8)', 0.5],
['rgba(255,255,255,0)', 1]
]
})
}
@LocalBuilder skeletonItem() {
Column({ space: 8 }) {
Row()
.width(60)
.height(60)
.backgroundColor('#ECECEC')
.borderRadius(8)
Row()
.width('80%')
.height(20)
.backgroundColor('#ECECEC')
.borderRadius(4)
Row()
.width('70%')
.height(20)
.backgroundColor('#ECECEC')
.borderRadius(4)
}
.padding(10)
.backgroundColor('#F7F8FA')
.borderRadius(8)
.width('100%')
.alignItems(HorizontalAlign.Start)
.overlay(this.shineOverlay())
.clip(true); // 必须加,否则流光会超出圆角
}
build() {
Column({ space: 12 }) {
ForEach(
this.skeletonCount,
(item: number) => {
this.skeletonItem()
},
(item: number) => item.toString()
)
}
.padding(20)
.width('100%')
.height('100%')
.backgroundColor('#F1F3F5')
}
}
运行效果

五、ForEach 性能优化技巧
- 键值优化:优先使用对象唯一 ID 作为键值,避免使用 index 或动态变化的字段;
- 组件复用:子组件尽量轻量化,避免在
itemGenerator中创建复杂组件(如嵌套多层布局); - 数据源优化:大数据量场景(千条以上)建议使用
LazyForEach(懒加载),而非 ForEach; - 避免混用:List/Grid 容器中,不要同时使用 ForEach 和 LazyForEach。
六、内容总结
- 核心逻辑:ForEach 基于键值判断组件创建/复用/销毁,键值必须唯一;
- 渲染机制:首次渲染创建所有组件,非首次渲染仅处理键值变化的组件;
- 场景选型:
- 静态列表:基本类型数组 + 数组项作为键值;
- 动态列表:对象数组 + 唯一 ID 作为键值;
- 避坑核心:不使用 index 作为键值,不省略
keyGenerator; - 性能优化:轻量化子组件、优化键值、大数据量用 LazyForEach、长列表用 List 容器。
七、代码仓库
- 工程名称:ForEachDemo
- 仓库地址:https://gitee.com/HarmonyOS-UI-Basics/harmony-os-ui-basics.git
八、下节预告
下一节我们将深入学习鸿蒙应用开发中的长列表性能优化核心 —— LazyForEach 懒加载实战,掌握千条级数据列表的流畅滚动实现方案。
更多推荐



所有评论(0)