ForEach接口基于数组类型数据来进行循环渲染,需要与容器组件配合使用,且接口返回的组件应当是允许包含在ForEach父容器组件中的子组件。例如,ListItem组件要求ForEach的父容器组件必须为 List组件 。
说明:
从API version 9开始,该接口支持在ArkTS卡片中使用。
接口描述
ForEach(
arr: Array,
itemGenerator: (item: any, index: number) => void,
keyGenerator?: (item: any, index: number) => string
)
以下是参数的详细说明:
参数名 | 参数类型 | 是否必填 | 参数描述 |
---|---|---|---|
arr | Array | 是 | 数据源,为Array 类型的数组。说明: - 可以设置为空数组,此时不会创建子组件。 - 可以设置返回值为数组类型的函数,例如 arr.slice(1, 3) ,但设置的函数不应改变包括数组本身在内的任何状态变量,例如不应使用Array.splice() ,Array.sort() 或Array.reverse() 这些会改变原数组的函数。 |
itemGenerator | (item: any, index: number) => void |
是 | 组件生成函数。 - 为数组中的每个元素创建对应的组件。 - item 参数:arr 数组中的数据项。- index 参数(可选):arr 数组中的数据项索引。说明: - 组件的类型必须是 ForEach 的父容器所允许的。例如,ListItem 组件要求ForEach 的父容器组件必须为List 组件。 |
keyGenerator | (item: any, index: number) => string |
否 | 键值生成函数。 - 为数据源 arr 的每个数组项生成唯一且持久的键值。函数返回值为开发者自定义的键值生成规则。- item 参数:arr 数组中的数据项。- index 参数(可选):arr 数组中的数据项索引。说明: - 如果函数缺省,框架默认的键值生成函数为 (item: T, index: number) => { return index + '__' + JSON.stringify(item); } - 键值生成函数不应改变任何组件状态。 |
说明:
-
ForEach
的itemGenerator
函数可以包含if/else
条件渲染逻辑。另外,也可以在if/else
条件渲染语句中使用ForEach
组件。 - 在初始化渲染时,
ForEach
会加载数据源的所有数据,并为每个数据项创建对应的组件,然后将其挂载到渲染树上。如果数据源非常大或有特定的性能需求,建议使用LazyForEach
组件。
键值生成规则
在ForEach
循环渲染过程中,系统会为每个数组元素生成一个唯一且持久的键值,用于标识对应的组件。当这个键值变化时,ArkUI框架将视为该数组元素已被替换或修改,并会基于新的键值创建一个新的组件。
ForEach
提供了一个名为keyGenerator
的参数,这是一个函数,开发者可以通过它自定义键值的生成规则。如果开发者没有定义keyGenerator
函数,则ArkUI框架会使用默认的键值生成函数,即(item: any, index: number) => { return index + '__' + JSON.stringify(item); }
。
ArkUI框架对于ForEach
的键值生成有一套特定的判断规则,这主要与itemGenerator
函数的第二个参数index
以及keyGenerator
函数的第二个参数index
有关,具体的键值生成规则判断逻辑如下图所示。
图1 ForEach键值生成规则
说明:
ArkUI框架会对重复的键值发出警告。在UI更新的场景下,如果出现重复的键值,框架可能无法正常工作。
组件创建规则
在确定键值生成规则后,ForEach的第二个参数itemGenerator
函数会根据键值生成规则为数据源的每个数组项创建组件。组件的创建包括两种情况: ForEach首次渲染 和 ForEach非首次渲染 。
首次渲染
在ForEach首次渲染时,会根据前述键值生成规则为数据源的每个数组项生成唯一键值,并创建相应的组件。
@Entry
@Component
struct Parent {
@State simpleList: Array<string> = ['one', 'two', 'three'];
build() {
Row() {
Column() {
ForEach(this.simpleList, (item: string) => {
ChildItem({ item: item })
}, (item: string) => item)
}
.width('100%')
.height('100%')
}
.height('100%')
.backgroundColor(0xF1F3F5)
}
}
@Component
struct ChildItem {
@Prop item: string;
build() {
Text(this.item)
.fontSize(50)
}
}
运行效果如下图所示。
图2 ForEach数据源不存在相同值案例首次渲染运行效果图
在上述代码中,键值生成规则是keyGenerator
函数的返回值item
。在ForEach渲染循环时,为数据源数组项依次生成键值one
、two
和three
,并创建对应的ChildItem
组件渲染到界面上。
当不同数组项按照键值生成规则生成的键值相同时,框架的行为是未定义的。例如,在以下代码中,ForEach渲染相同的数据项two
时,只创建了一个ChildItem
组件,而没有创建多个具有相同键值的组件。
@Entry
@Component
struct Parent {
@State simpleList: Array<string> = ['one', 'two', 'two', 'three'];
build() {
Row() {
Column() {
ForEach(this.simpleList, (item: string) => {
ChildItem({ item: item })
}, (item: string) => item)
}
.width('100%')
.height('100%')
}
.height('100%')
.backgroundColor(0xF1F3F5)
}
}
@Component
struct ChildItem {
@Prop item: string;
build() {
Text(this.item)
.fontSize(50)
}
}
运行效果如下图所示。
图3 ForEach数据源存在相同值案例首次渲染运行效果图
在该示例中,最终键值生成规则为item
。当ForEach遍历数据源simpleList
,遍历到索引为1的two
时,按照最终键值生成规则生成键值为two
的组件并进行标记。当遍历到索引为2的two
时,按照最终键值生成规则当前项的键值也为two
,此时不再创建新的组件。
非首次渲染
在ForEach组件进行非首次渲染时,它会检查新生成的键值是否在上次渲染中已经存在。如果键值不存在,则会创建一个新的组件;如果键值存在,则不会创建新的组件,而是直接渲染该键值所对应的组件。例如,在以下的代码示例中,通过点击事件修改了数组的第三项值为"new three",这将触发ForEach组件进行非首次渲染。
@Entry
@Component
struct Parent {
@State simpleList: Array<string> = ['one', 'two', 'three'];
build() {
Row() {
Column() {
Text('点击修改第3个数组项的值')
.fontSize(24)
.fontColor(Color.Red)
.onClick(() => {
this.simpleList[2] = 'new three';
})
ForEach(this.simpleList, (item: string) => {
ChildItem({ item: item })
.margin({ top: 20 })
}, (item: string) => item)
}
.justifyContent(FlexAlign.Center)
.width('100%')
.height('100%')
}
.height('100%')
.backgroundColor(0xF1F3F5)
}
}
@Component
struct ChildItem {
@Prop item: string;
build() {
Text(this.item)
.fontSize(30)
}
}
运行效果如下图所示。
图4 ForEach非首次渲染案例运行效果图
从本例可以看出@State
能够监听到简单数据类型数组数据源 simpleList
数组项的变化。
- 当
simpleList
数组项发生变化时,会触发ForEach
进行重新渲染。 -
ForEach
遍历新的数据源['one', 'two', 'new three']
,并生成对应的键值one
、two
和new three
。 - 其中,键值
one
和two
在上次渲染中已经存在,所以ForEach
复用了对应的组件并进行了渲染。对于第三个数组项 “new three”,由于其通过键值生成规则item
生成的键值new three
在上次渲染中不存在,因此ForEach
为该数组项创建了一个新的组件。
使用场景
ForEach组件在开发过程中的主要应用场景包括: 数据源不变 、 数据源数组项发生变化 (如插入、删除操作)、 数据源数组项子属性变化 。
数据源不变
在数据源保持不变的场景中,数据源可以直接采用基本数据类型。例如,在页面加载状态时,可以使用骨架屏列表进行渲染展示。
@Entry
@Component
struct ArticleList {
@State simpleList: Array<number> = [1, 2, 3, 4, 5];
build() {
Column() {
ForEach(this.simpleList, (item: string) => {
ArticleSkeletonView()
.margin({ top: 20 })
}, (item: string) => item)
}
.padding(20)
.width('100%')
.height('100%')
}
}
@Builder
function textArea(width: number | Resource | string = '100%', height: number | Resource | string = '100%') {
Row()
.width(width)
.height(height)
.backgroundColor('#FFF2F3F4')
}
@Component
struct ArticleSkeletonView {
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('#FFECECEC')
.height(120)
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
}
运行效果如下图所示。
图5 骨架屏运行效果图
在本示例中,采用数据项item作为键值生成规则,由于数据源simpleList的数组项各不相同,因此能够保证键值的唯一性。
数据源数组项发生变化
在数据源数组项发生变化的场景下,例如进行数组插入、删除操作或者数组项索引位置发生交换时,数据源应为对象数组类型,并使用对象的唯一ID作为最终键值。例如,当在页面上通过手势上滑加载下一页数据时,会在数据源数组尾部新增新获取的数据项,从而使得数据源数组长度增大。
class Article {
id: string;
title: string;
brief: string;
constructor(id: string, title: string, brief: string) {
this.id = id;
this.title = title;
this.brief = brief;
}
}
@Entry
@Component
struct ArticleListView {
@State isListReachEnd: boolean = false;
@State articleList: Array<Article> = [
new Article('001', '第1篇文章', '文章简介内容'),
new Article('002', '第2篇文章', '文章简介内容'),
new Article('003', '第3篇文章', '文章简介内容'),
new Article('004', '第4篇文章', '文章简介内容'),
new Article('005', '第5篇文章', '文章简介内容'),
new Article('006', '第6篇文章', '文章简介内容')
]
loadMoreArticles() {
this.articleList.push(new Article('007', '加载的新文章', '文章简介内容'));
}
build() {
Column({ space: 5 }) {
List() {
ForEach(this.articleList, (item: Article) => {
ListItem() {
ArticleCard({ article: item })
.margin({ top: 20 })
}
}, (item: Article) => item.id)
}
.onReachEnd(() => {
this.isListReachEnd = true;
})
.parallelGesture(
PanGesture({ direction: PanDirection.Up, distance: 80 })
.onActionStart(() => {
if (this.isListReachEnd) {
this.loadMoreArticles();
this.isListReachEnd = false;
}
})
)
.padding(20)
.scrollBar(BarState.Off)
}
.width('100%')
.height('100%')
.backgroundColor(0xF1F3F5)
}
}
@Component
struct ArticleCard {
@Prop article: Article;
build() {
Row() {
Image($r('app.media.icon'))
.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%')
.height('100%')
}
.padding(20)
.borderRadius(12)
.backgroundColor('#FFECECEC')
.height(120)
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
}
初始运行效果(左图)和手势上滑加载后效果(右图)如下图所示。
图6 数据源数组项变化案例运行效果图
在本示例中,ArticleCard
组件作为ArticleListView
组件的子组件,通过@Prop
装饰器接收一个Article
对象,用于渲染文章卡片。
- 当列表滚动到底部时,如果手势滑动距离超过指定的80,将触发
loadMoreArticle()
函数。此函数会在articleList
数据源的尾部添加一个新的数据项,从而增加数据源的长度。 - 数据源被
@State
装饰器修饰,ArkUI框架能够感知到数据源长度的变化,并触发ForEach
进行重新渲染。
数据源数组项子属性变化
当数据源的数组项为对象数据类型,并且只修改某个数组项的属性值时,由于数据源为复杂数据类型,ArkUI框架无法监听到@State
装饰器修饰的数据源数组项的属性变化,从而无法触发ForEach
的重新渲染。为实现ForEach
重新渲染,需要结合@Observed
和@ObjectLink
装饰器使用。例如,在文章列表卡片上点击“点赞”按钮,从而修改文章的点赞数量。
@Observed
class Article {
id: string;
title: string;
brief: string;
isLiked: boolean;
likesCount: number;
constructor(id: string, title: string, brief: string, isLiked: boolean, likesCount: number) {
this.id = id;
this.title = title;
this.brief = brief;
this.isLiked = isLiked;
this.likesCount = likesCount;
}
}
@Entry
@Component
struct ArticleListView {
@State articleList: Array<Article> = [
new Article('001', '第0篇文章', '文章简介内容', false, 100),
new Article('002', '第1篇文章', '文章简介内容', false, 100),
new Article('003', '第2篇文章', '文章简介内容', false, 100),
new Article('004', '第4篇文章', '文章简介内容', false, 100),
new Article('005', '第5篇文章', '文章简介内容', false, 100),
new Article('006', '第6篇文章', '文章简介内容', false, 100),
];
build() {
List() {
ForEach(this.articleList, (item: Article) => {
ListItem() {
ArticleCard({
article: item
})
.margin({ top: 20 })
}
}, (item: Article) => item.