【学习目标】

  • 掌握 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 识别组件唯一性的核心,用于判断组件是否需要创建/复用/销毁:

  1. 若自定义 keyGenerator:需返回唯一且持久的字符串(如对象的唯一 ID);
  2. 若未自定义:框架默认生成规则为 index + '__' + JSON.stringify(item)
  3. 键值重复后果:框架仅创建第一个重复键值对应的组件,后续重复项不渲染;
  4. 键值变化后果:框架视为组件替换,销毁旧组件并创建新组件。

三、ForEach 渲染机制详解

3.1 首次渲染

遍历数据源,为每个数组项生成唯一键值,根据键值创建对应组件并渲染,流程:

  1. 遍历数组每一项;
  2. 调用 keyGenerator 生成键值;
  3. 若键值未存在,创建组件并记录键值;
  4. 若键值已存在,跳过组件创建(避免重复渲染)。
自定义列表项
// 子组件:自定义列表项
@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'))
  }
}
首次渲染—无重复项

ForEach首次渲染

修改数据源
// 数据源包含重复项
@State fruitList: string[] = ['苹果', '香蕉', '香蕉', '葡萄'];
首次渲染—有重复项

仅渲染「苹果、香蕉、葡萄」三项,重复的「香蕉」因键值重复被跳过。

3.2 非首次渲染(数据源变化后)

当数据源发生变化(增删改查)时,框架触发重新渲染,流程:

  1. 遍历新数据源,生成新键值列表;
  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

scrollerForEach

检查视图

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

ForEach全部创建

使用List+ForEach(listBuilder)
// this.scrollerBuilder()
this.listBuilder()
再次运行查看日志

配合List组件后,循环渲染性能有所提升。从0创建到23,并未全部创建。页面可视区域显示11条(0-10),不可见区域预创建13条。

List+ForEach

检查UI视图

我们发现List+ForEach循环渲染组件性能大幅提升。虽然首次运行创建了23个组件,可视区域显示10条,但不可见区域的13条并未直接挂载到组件树上。
List+ForEach性能提升

点击修改第三项为芒果

销毁原来的组件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 性能优化技巧

  1. 键值优化:优先使用对象唯一 ID 作为键值,避免使用 index 或动态变化的字段;
  2. 组件复用:子组件尽量轻量化,避免在 itemGenerator 中创建复杂组件(如嵌套多层布局);
  3. 数据源优化:大数据量场景(千条以上)建议使用 LazyForEach(懒加载),而非 ForEach;
  4. 避免混用:List/Grid 容器中,不要同时使用 ForEach 和 LazyForEach。

六、内容总结

  1. 核心逻辑:ForEach 基于键值判断组件创建/复用/销毁,键值必须唯一;
  2. 渲染机制:首次渲染创建所有组件,非首次渲染仅处理键值变化的组件;
  3. 场景选型
    • 静态列表:基本类型数组 + 数组项作为键值;
    • 动态列表:对象数组 + 唯一 ID 作为键值;
  4. 避坑核心:不使用 index 作为键值,不省略 keyGenerator
  5. 性能优化:轻量化子组件、优化键值、大数据量用 LazyForEach、长列表用 List 容器。

七、代码仓库

  • 工程名称:ForEachDemo
  • 仓库地址:https://gitee.com/HarmonyOS-UI-Basics/harmony-os-ui-basics.git

八、下节预告

下一节我们将深入学习鸿蒙应用开发中的长列表性能优化核心 —— LazyForEach 懒加载实战,掌握千条级数据列表的流畅滚动实现方案。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐