文档知识库教程OpenHarmony【免费下载链接】CangjieCorpus本仓库为仓颉编程语言语料库聚焦 仓颉编程语言 核心知识体系整合官方开发指南、API 文档、典型代码示例及语法规范等多维度资源经结构化处理与专业标注构建为支持 RAG检索增强生成技术 的高质量知识基座适配智能编码助手、学习辅助工具等应用开发需求。 欢迎开发者基于本语料库构建仓颉语言智能应用推动鸿蒙开发生态智能化升级项目地址https://gitcode.com/Cangjie/CangjieCorpus点击查看免费下载ForEach 是 ArkUI 声明式 UI 中基于数组数据源进行循环渲染的核心接口它通过与容器组件如 List、Grid、Column 等配合把数据数组批量转换为可交互的 UI 组件树。本文以仓颉Cangjie语言实现为例系统讲解 ForEach 的键值生成规则、首次与非首次渲染流程、两类典型使用场景数据源不变 / 数据源数组项变化并结合完整可运行示例剖析键值规则包含 index和缺省 keyGenerator两类不推荐写法背后的渲染原理与性能代价帮助开发者在鸿蒙应用中写出正确且高效的循环渲染代码。ForEach 基本用法与适用边界ForEach 接口基于数组类型数据做循环渲染必须与容器组件配合使用且接口返回的组件应当是允许包含在 ForEach 父容器组件中的子组件。例如 ListItem 组件 要求 ForEach 的父容器必须为 List 组件同理Grid、Swiper 等容器内的 ForEach 也要保证生成的子组件类型合法。从 渲染控制概述 可知ArkUI 的渲染控制语句还包括条件渲染、面向大数据量的数据懒加载LazyForEach等。ForEach 与之互补它是全量渲染模型适用于数据量中等、组件结构简单的场景当列表项数量很大时应评估使用 LazyForEach 结合缓存、预加载、组件复用等手段提升滑动帧率、降低内存占用。在 List、Grid、Swiper 内使用 ForEach 时不要与 LazyForEach 混用同一容器内同时包含 ForEach 与 LazyForEach 是不推荐的写法。ForEach 的典型调用形态如下以仓颉代码为例ForEach(this.simpleList, itemGeneratorFunc: {item: String, idx: Int64 ChildItem(item: item)}, keyGeneratorFunc: {item: String, idx: Int64 return item})其中第二个参数itemGeneratorFunc负责为每个数组项创建组件第三个参数keyGeneratorFunc负责为每个数组项生成键值这两个参数都接收(item, index)两个入参。键值生成规则唯一且持久的组件标识在 ForEach 循环渲染过程中系统会为每个数组元素生成一个唯一且持久的键值用于标识对应的组件。当这个键值变化时仓颉将视为该数组元素已被替换或修改并会基于新的键值创建一个新的组件。ForEach 提供名为keyGenerator仓颉代码中写作keyGeneratorFunc的函数参数开发者可以通过它自定义键值的生成规则若开发者没有定义该函数则 ArkUI 框架会使用默认的键值生成函数。ArkUI 框架对于 ForEach 的键值生成有一套特定的判断规则这套规则主要与itemGeneratorFunc的第二个参数index以及keyGeneratorFunc的第二个参数index有关。理解这层关系是正确使用 ForEach 的前提默认键值生成规则缺省keyGenerator时框架会以index 前缀 数组项内容组合生成键值如0__one、1__two、2__three。由于默认规则包含索引数据源一旦在中间位置插入/删除项后续所有项的键值都会变化触发大范围组件重建。自定义键值规则开发者可基于数组项自身的唯一字段如对象的id生成稳定键值使键值与索引解耦从而在数据项增删时最大化组件复用。说明ArkUI 框架会对重复的键值发出警告。在 UI 更新的场景下如果出现重复的键值框架可能无法正常工作具体表现可参见下文 渲染结果非预期 一节。组件创建规则与首次渲染在确定键值生成规则后itemGeneratorFunc会根据键值生成规则为数据源的每个数组项创建组件。组件的创建分两种情况首次渲染与非首次渲染。在 ForEach 首次渲染时系统根据键值生成规则为数据源的每个数组项生成唯一键值并创建相应的组件。下面是一个最简示例数据源为三个不重复的字符串键值规则直接返回item。package ohos_app_cangjie_entry import kit.ArkUI.* import ohos.arkui.state_macro_manage.* Component class ChildItem { Prop var item: String func build() { Text(this.item) .fontSize(50) } } Entry Component class EntryView { State var simpleList: ArrayString [one, two,three] func build() { Row() { Column() { ForEach(this.simpleList,itemGeneratorFunc: {item: String,idx:Int64 ChildItem(item: item)}, keyGeneratorFunc: {item: String, idx: Int64 return item}) } .justifyContent(FlexAlign.Center) .width(100.percent) .height(100.percent) } .height(100.percent) .backgroundColor(Color.White) } }在上述代码中键值生成规则是keyGeneratorFunc的返回值item。ForEach 在渲染循环时为数据源数组项依次生成键值one、two和three并创建对应的ChildItem组件渲染到界面上。需要特别警惕的是当不同数组项按照键值生成规则生成的键值相同时框架的行为是未定义的。例如把数据源改为[one, two, two, three]ForEach 渲染相同的数据项two时只创建了一个ChildItem组件而不会创建多个具有相同键值的组件package ohos_app_cangjie_entry import kit.ArkUI.* import ohos.arkui.state_macro_manage.* Entry Component class EntryView { State var simpleList: ArrayString [one, two,two,three] func build() { Row() { Column() { ForEach(this.simpleList,itemGeneratorFunc: {item: String,idx:Int64 ChildItem(item: item)}, keyGeneratorFunc: {item: String, idx: Int64 return item}) } .justifyContent(FlexAlign.Center) .width(100.percent) .height(100.percent) } .height(100.percent) .backgroundColor(Color.White) } }该示例中最终键值生成规则为item。当 ForEach 遍历数据源simpleList遍历到索引为 1 的two时生成键值two并创建组件、进行标记当遍历到索引为 2 的two时按规则生成的键值仍为two已存在此时不再创建新的组件。因此界面只渲染出一个two与数据源中出现的两次two不匹配——这是开发者自定义键值规则时必须规避的重复键值陷阱。非首次渲染键值复用决定组件复用在 ForEach 组件进行非首次渲染时它会检查新生成的键值是否在上次渲染中已经存在键值不存在→ 创建新的组件键值已存在→ 不创建新组件直接渲染该键值所对应的既有组件组件被复用。下面的示例通过点击事件修改数组的第三项值为new three从而触发 ForEach 进行非首次渲染package ohos_app_cangjie_entry import kit.ArkUI.* import ohos.arkui.state_macro_manage.* Component class ChildItem { Prop var item: String func build() { Text(this.item) .fontSize(50) } } Entry Component class EntryView { State var simpleList: ObservedArrayListString ObservedArrayListString([one, two,three]) func build() { Row() { Column() { Text(点击修改第3个数组项的值) .fontSize(24) .fontColor(Color.Red) .onClick({evt this.simpleList[2] new three }) ForEach(this.simpleList,itemGeneratorFunc: {item: String,idx:Int64 ChildItem(item: item)}, keyGeneratorFunc: {item: String, idx: Int64 return item}) } .justifyContent(FlexAlign.Center) .width(100.percent) .height(100.percent) } .height(100.percent) .backgroundColor(Color.White) } }此例把数据源从Array换成ObservedArrayList这背后依赖的是 State 状态管理宏State能够监听到简单数据类型数组数据源simpleList数组项的变化。整个非首次渲染过程可拆解为三步当simpleList数组项发生变化时触发 ForEach 进行重新渲染ForEach 遍历新的数据源[one, two, new three]并生成对应的键值one、two和new three键值one、two在上次渲染中已存在因此 ForEach 复用对应组件并渲染第三个数组项new three按规则生成的键值new three在上次渲染中不存在因此 ForEach 为该数组项创建了一个新组件。由此可见键值是否稳定直接决定了数据变更时组件是复用还是重建——这正是 ForEach 性能调优的核心抓手。使用场景一数据源不变骨架屏等静态渲染ForEach 的主要应用场景包括两类数据源不变、数据源数组项发生变化如插入、删除操作。在数据源保持不变的场景中数据源可以直接采用基本数据类型。例如页面加载状态时使用骨架屏列表进行渲染展示此时数据只用于控制渲染多少个占位块内容本身不参与业务变化package ohos_app_cangjie_entry import kit.ArkUI.* import ohos.arkui.state_macro_manage.* Builder func textArea(width: Int64, height:Int64) { Row() .width(width) .height(height) .backgroundColor(Color.White) } Component class ArticleSkeletonView { func build() { Row() { Column() { textArea(80, 80) } .margin(right: 20 ) Column() { textArea(60, 20) textArea(50, 20) } .alignItems(HorizontalAlign.Start) .justifyContent(FlexAlign.SpaceAround) .height(100) } .padding(20) .borderRadius(12) .backgroundColor(Color.Gray) .height(120) .width(100.percent) .justifyContent(FlexAlign.SpaceBetween) .margin(top: 20) } } Entry Component class EntryView { State var simpleList: ArrayInt64 [1, 2, 3, 4, 5] func build() { Column() { ForEach(this.simpleList, itemGenerator: {item: Int64,idx:Int64 ArticleSkeletonView()} ) } .padding(20) .width(100.percent) .height(100.percent) } }这里Builder宏把骨架屏的占位区域textArea封装为可复用片段ForEach依据simpleList5 个整数生成 5 个ArticleSkeletonView占位卡片。由于数据源在此场景下从不变化基本数据类型直接充当数组项是安全且简洁的选择。使用场景二数据源数组项发生变化列表上拉加载更多在数据源数组项发生变化的场景下数组插入、删除操作或数组项索引位置发生交换数据源应为对象数组类型并使用对象的唯一 ID 作为最终键值。典型例子是页面通过手势上滑加载下一页数据在数据源数组尾部追加新获取的数据项使数据源数组长度增大。package ohos_app_cangjie_entry import kit.ArkUI.* import ohos.arkui.state_macro_manage.* import std.collection.ArrayList import kit.LocalizationKit.* import kit.PerformanceAnalysisKit.Hilog import ohos.resource.__GenerateResource__ class Article { var id: String var title: String var brief: String init(id: String, title: String, brief: String) { this.id id this.title title this.brief brief } } Builder func textArea(width: Int64, height:Int64) { Row() .width(width) .height(height) .backgroundColor(Color.White) } Component class ArticleCard { Prop var article: Article func build() { Row(){ // 此处app.media.icon仅作示例请开发者自行替换否则imageSource创建失败会导致后续无法正常执行。 Image(r(app.media.startIcon)) .width(80) .height(80) .margin(right:20) Column() { Text(this.article.title) .fontSize(20) .margin(bottom:8) Text(this.article.brief) .fontSize(16) .fontColor(Color.Gray) .margin(bottom: 8) } .alignItems(HorizontalAlign.Start) .width(80.percent) .height(100.percent) } .padding(20) .borderRadius(12) .backgroundColor(Color.White) .height(120) .width(100.percent) .justifyContent(FlexAlign.SpaceBetween) .margin(top: 20) } } Entry Component class EntryView { State var isListReachEnd: Bool false State var articleList: ObservedArrayListArticle ObservedArrayListArticle([ Article(001, 第1篇文章, 文章简介内容), Article(002, 第2篇文章, 文章简介内容), Article(003, 第3篇文章, 文章简介内容), Article(004, 第4篇文章, 文章简介内容), Article(005, 第5篇文章, 文章简介内容), Article(006, 第6篇文章, 文章简介内容) ]) func loadMoreArticles() { this.articleList.append(Article(007, 加载的新文章, 文章简介内容)) Hilog.info(0,here,here) } func build() { Column(space: 5) { List() { ForEach(this.articleList, itemGeneratorFunc: {item: Article,idx:Int64 ListItem() { ArticleCard(article: item) } }, keyGeneratorFunc: {item: Article, idx: Int64 return item.id}) } .onReachEnd( { this.isListReachEnd true }) .padding(20) .scrollBar(BarState.Off) } .width(100.percent) .height(100.percent) .backgroundColor(Color.Gray) } }在本示例中ArticleCard组件作为 List 中子组件通过Prop装饰器接收一个Article对象用于渲染文章卡片。其工作流程为当列表滚动到底部onReachEnd触发时调用loadMoreArticles()函数在articleList数据源尾部append一个新的Article(007, ...)使数据源长度增大数据源被State装饰器修饰ArkUI 框架能够感知到数据源长度的变化并触发 ForEach 进行重新渲染键值规则为item.id新项007是全新键值ForEach 只需创建这一个新组件其余 6 个已有卡片全部复用插入成本最低。这里要特别说明两点工程细节Image(r(app.media.startIcon))中的资源引用仅作示例开发者必须替换为自己的媒体资源否则 imageSource 创建失败会导致后续无法正常执行Hilog.info来自 [kit.PerformanceAnalysisKit.Hilog]用于在真机/DevEco Studio 日志中观察加载行为实际项目中可改为合理的业务日志。使用建议键值规则的设计准则综合以上原理ForEach 的键值设计应遵循以下准则对象数据用唯一 id 作键值为满足键值的唯一性对于对象数据类型建议使用对象数据中的唯一 id 作为键值如item.id。避免在最终键值规则中包含 index尽量避免在键值生成规则中混入数据项索引index以防止出现渲染结果非预期和渲染性能降低。如果业务确实需要index例如列表需要通过 index 进行条件渲染开发者需要接受 ForEach 在改变数据源后重新创建组件所带来的性能损耗。基本数据类型防重复基本数据类型的数据项没有唯一 ID 属性。如果使用基本数据类型本身作为键值必须确保数组项无重复。因此对于数据源会发生变化的场景建议将基本数据类型数组转化为具备唯一 ID 属性的对象数据类型数组再用唯一 ID 属性作为键值。index 参数的正确角色在上述限制规则下index参数存在的意义在于——它是开发者保证键值唯一性的最终手段同时由于itemGenerator中的item参数是不可修改的对数据项进行修改时必须借助index索引值去修改数据源进而触发 UI 重新渲染。禁止与 LazyForEach 混用ForEach 在 List、Grid、Swiper 等容器内使用时不要与 LazyForEach 混用。以 List 为例同时包含 ForEach、LazyForEach 的情形是不推荐的。避免内容相同的新项替换旧项数组项是对象数据类型的情况下不建议用内容相同的数组项替换旧的数组项会造成键值冲突或组件内容无法正确区分。不推荐案例一键值规则包含 index 导致渲染结果非预期开发者对键值生成规则理解不充分时错误使用一方面会造成功能层面问题渲染结果非预期另一方面会造成性能层面问题渲染性能降低。先看渲染结果非预期的案例。通过设置第三个参数keyGenerator函数把键值规则自定义为数据源的索引index的字符串类型值点击父组件中的在第 1 项后插入新项按钮后界面出现非预期结果package ohos_app_cangjie_entry import kit.ArkUI.* import ohos.arkui.state_macro_manage.* import std.collection.ArrayList import kit.LocalizationKit.* import kit.PerformanceAnalysisKit.Hilog Component class ChildItem { Prop var item: String func build() { Text(this.item) .fontSize(30) } } Entry Component class EntryView { State var simpleList: ObservedArrayListString ObservedArrayList([one, two, three]) func build() { Column() { Button() { Text(在第1项后插入新项).fontSize(30) } .onClick({ evt this.simpleList.insert(1,new item) }) ForEach(this.simpleList, itemGeneratorFunc: {item: String ,idx:Int64 ChildItem(item: item) },keyGeneratorFunc: {item: String, index: Int64 index.toString()} ) } .justifyContent(FlexAlign.Center) .width(100.percent) .height(100.percent) .backgroundColor(Color.White) } }我们来逐步还原框架的执行过程ForEach 首次渲染时创建的键值依次为0、1、2插入新项后数据源simpleList变为[one, new item, two, three]框架监听到 State 装饰的数据源长度变化触发 ForEach 重新渲染ForEach 依次遍历新数据源遍历one生成键值0已存在 → 不创建新组件旧组件上渲染的仍是one恰好匹配遍历new item生成键值1已存在 → 不创建新组件但旧键值1对应的是two内容不匹配遍历two生成键值2已存在 → 不创建新组件旧键值2对应的是three内容不匹配遍历three生成键值3不存在 → 创建内容为three的新组件并渲染。最终期望的界面渲染结果为[one, new item, two, three]而实际渲染结果为[one, two, three, three]。其根因是键值绑定在易变的索引上插入操作使所有后续项的键值整体错位而 ForEach 只认键值、不认内容。因此开发者在使用 ForEach 时应尽量避免最终键值生成规则中包含 index。不推荐案例二缺省 keyGenerator 导致渲染性能降低再看渲染性能降低的案例。此例中 ForEach 的第三个参数keyGenerator处于缺省状态点击在第 1 项后插入新项后ForEach 需要为第 2 个数组项以及其后的所有项重新创建组件package ohos_app_cangjie_entry import kit.ArkUI.* import ohos.arkui.state_macro_manage.* import std.collection.ArrayList import kit.LocalizationKit.* import kit.PerformanceAnalysisKit.Hilog Component class ChildItem { Prop var item: String protected override func aboutToAppear() { Hilog.info(0,0,[aboutToAppear]: item is ${this.item}) } func build() { Text(this.item) .fontSize(30) } } Entry Component class EntryView { State var simpleList: ObservedArrayListString ObservedArrayList([one, two, three]) func build() { Column() { Button() { Text(在第1项后插入新项).fontSize(30) } .onClick({ evt this.simpleList.insert(1,new item) }) ForEach(this.simpleList, itemGenerator: {item: String ,idx:Int64 ChildItem(item: item)} ) } .justifyContent(FlexAlign.Center) .width(100.percent) .height(100.percent) .backgroundColor(Color.White) } }该示例在ChildItem中重写了aboutToAppear()生命周期函数并用Hilog打印日志从而可以在 DevEco Studio 日志中直观观察到组件的创建行为。插入新项后ForEach 为new item、two、three三个数组项都创建了对应组件并执行了aboutToAppear()。原因如下首次渲染时默认键值生成规则生成的键值依次为0__one、1__two、2__three插入新项后数据源变为[one, new item, two, three]State 监听到长度变化触发 ForEach 重新渲染ForEach 依次遍历one生成键值0__one已存在 → 不创建新组件new item生成键值1__new item不存在 → 创建新组件并渲染two生成键值2__two不存在 → 创建新组件并渲染three生成键值3__three不存在 → 创建新组件并渲染。尽管此示例中界面渲染结果符合预期但每次插入一条新数组项时ForEach 都会为从该数组项起后面的所有数组项全部重新创建组件。当数据源数据量较大或组件结构复杂时组件无法复用将导致性能体验不佳。因此除非必要否则不推荐将第三个参数keyGenerator处于缺省状态也不推荐在键值生成规则中包含索引 index。正确且高效的 ForEach 写法是为每个数据项生成稳定唯一的键值例如ForEach(this.simpleList, itemGenerator: {item: String ,idx:Int64 ChildItem(item: item)}, keyGenerator: {item: String, index: Int64 item})提供了第三个参数keyGenerator后在本例中它对数据源的不同数据项生成不同的 key并且对同一个数据项每次生成相同的 key——这正是 ForEach 高效复用的底层保证键值稳定 → 组件复用 → 渲染成本最小化。小结ForEach 循环渲染的正确性与性能全部系于键值设计这一件事场景数据源类型推荐键值理由数据源不变如骨架屏基本数据类型数组可缺省或使用数组项本身无增删键值始终稳定数据源数组项变化插入/删除/交换对象数组对象唯一 id键值不随索引漂移最大程度复用组件需条件渲染、必须用 index任意接受重建代价index 是保证键值唯一性的最后手段核心结论可归纳为三条其一键值必须唯一且持久重复键值会导致框架行为未定义其二键值生成规则中混入 index 会造成渲染结果非预期缺省 keyGenerator 会造成插入点之后全量重建的性能损耗其三ForEach 适用于中等规模的全量渲染场景大数据量场景应改用 LazyForEach且二者不可在同一容器内混用。掌握键值规则即可在仓颉 ArkUI 应用中写出既正确又高效的循环渲染代码。赞分享文档知识库教程OpenHarmony【免费下载链接】CangjieCorpus本仓库为仓颉编程语言语料库聚焦 仓颉编程语言 核心知识体系整合官方开发指南、API 文档、典型代码示例及语法规范等多维度资源经结构化处理与专业标注构建为支持 RAG检索增强生成技术 的高质量知识基座适配智能编码助手、学习辅助工具等应用开发需求。 欢迎开发者基于本语料库构建仓颉语言智能应用推动鸿蒙开发生态智能化升级项目地址https://gitcode.com/Cangjie/CangjieCorpus点击查看免费下载相关推荐仓颉语言 ArkUI 键鼠事件开发指南onHover、onMouse 与 onKeyEvent 全解析仓颉语言 ArkUI 键鼠事件开发指南onHover、onMouse 与 onKeyEvent 全解析 导读 本篇指南以 OpenHarmony 仓颉Can文档知识库教程OpenHarmony仓颉 ArkUI 按钮组件Button开发指南创建、样式、事件与场景实战仓颉 ArkUI 按钮组件Button开发指南创建、样式、事件与场景实战 Button 是 ArkUI 中最常用的交互组件用于响应用户点击操作支持胶囊文档知识库教程OpenHarmony仓颉编程语言函数重载完全指南定义边界、决议机制与实战避坑仓颉编程语言函数重载完全指南定义边界、决议机制与实战避坑 函数重载Function Overloading是仓颉编程语言中一函数名、多函数定义的语言机文档知识库教程OpenHarmony上一篇imgaug KeepSizeByResize 单张图片数组输入崩溃问题Issue 590详解下一篇Cloudflare Skills零信任实战Cloudflare OneSASE的设计、配置与从Zscaler迁移创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考