条件渲染/循环渲染
条件渲染
使用规则
- 支持if、else和else if语句。
- if、else if语句后跟随的条件语句可以使用状态变量或者常规变量
- 允许在容器组件内使用,通过条件语句构建不同的子组件
- 条件渲染语句在组件的父子关系中是透明的
- 每个分支内部,都需要遵循构建函数的规则,创建一个或者多个组件
- 某些容器组件限制子组件的类型或数量,在条件渲染语句中,也将遵守这些规则。
更新机制
当if、else if后跟随的状态判断中使用的状态变量值发生变化时,条件渲染语句会进行更新
- 重新评估判断条件,分支是否有变化
- 如果分支发生变化,删除此前构建的所有子组件,同时执行新分支的构造函数,将新组建添加到父容器中
循环渲染
ForEach(this.arr, (item: string, index) => {
Text(item)
.width(50)
.height(50)
}, (item: string, index: number) => {
return item
})
键值生成规则
在ForEach循环渲染过程中,系统会为每个数组元素生成一个唯一且持久的键值,用于标识对应组件。当这个键值发生变化时,ArkUI框架将视为该数组的元素被替换或者修改,并会基于新的键值创建一个新的组件。
ForEach提供了一个名为keyGenerator的参数,这是一个函数,可以通过该函数自定义键值的生成规则。ArkUI框架的默认键值生成函数为:(item: Object, index: number) => {return index + '_' + JSON.stringify(item);}
ForEach键值生成规则:
组件创建规则
在确定键值生成规则后,ForEach的第二个参数itemGenerator函数会根据键值生成规则为数据源的每个数据项创建组件。
首次渲染
在ForEach首次渲染时,会根据ForEach会为每项数据生成唯一键值,并创建对应的组件。
如果keyGenerator函数返回的值存在相同的情况下,框架只会创建第一个组件,不会创建多个拥有相同键值的组件。
非首次渲染
在ForEach组件进行非首次渲染时,会检查新生成的键值是否已经存在,如果不存在,会创建一个新的组件,如果已经存在,则直接渲染该键值所对应的组件。
使用场景
数据源不变
在数据源保持不变的场景中,数据源可以直接采用基本数据类型。例如,在页面加载状态时,可以使用骨架屏列表进行渲染展示。
数据源数组项发生变化
在数据源数组项发生变化的场景下,例如进行数组插入、删除或者数组项索引位置发生交换时,数据源应为对象数组类型,自定义ForEach中的第三个参数keyGenerator函数,使用对象的唯一ID作为最终键值。
数据源数组项子属性变化
当数据源的数组项为对象类型,并且只修改了某个属性时,ArkUI框架无法监听到@State装饰器修饰的数据源数组项的属性变化,导致无法触发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.id)
}
.padding(20)
.scrollBar(BarState.Off)
.backgroundColor(0xF1F3F5)
}
}
@Component
struct ArticleCard {
@ObjectLink article: Article;
handleLiked() {
this.article.isLiked = !this.article.isLiked;
this.article.likesCount = this.article.isLiked ? this.article.likesCount + 1 : this.article.likesCount - 1;
}
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 })
Row() {
Image(this.article.isLiked ? $r('app.media.iconLiked') : $r('app.media.iconUnLiked'))
.width(24)
.height(24)
.margin({ right: 8 })
Text(this.article.likesCount.toString())
.fontSize(16)
}
.onClick(() => this.handleLiked())
.justifyContent(FlexAlign.Center)
}
.alignItems(HorizontalAlign.Start)
.width('80%')
.height('100%')
}
.padding(20)
.borderRadius(12)
.backgroundColor('#FFECECEC')
.height(120)
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
}
在上述示例中,Article类被@Observed装饰器修饰。父组件ArticleListView传入Article对象实例到子组件ArticleCard中,子组件使用@ObjectLink装饰器接收该实例。
- 当子组件的article实例属性发生变更时,由于该实例使用了@ObjectLink装饰器,父子组件共享同一份数据,因此,父组件articleList中的对应数据也会修改。
- 由于Article类被@Observed装饰器修饰,所以在属性发生变化时,可以触发ForEach重新渲染
- ForEach重新渲染时,发现当前数据源中的id均没有变化,所以不会新建组件
- 渲染各个子组件时,发现对应article对象属性值发生变化,触发UI展示变化。
拖拽排序
当ForEach在List组件下使用,并且设置了onMove事件,ForEach每次迭代都生成一个ListItem时,可以使其能拖拽排序。拖拽排序离手后,如果数据位置发生变化,则会触发onMove事件,上报数据移动起始索引号和目标索引号。在onMove事件中,需要根据上报的起始索引号和目标索引号修改数据源。数据源修改前后,要保持每个数据的键值不变,只是顺序发生变化,才能保证落位动画正常执行。
@Entry
@Component
struct ForEachSort {
@State arr: Array<string> = [];
build() {
Row() {
List() {
ForEach(this.arr, (item: string) => {
ListItem() {
Text(item.toString())
.fontSize(16)
.textAlign(TextAlign.Center)
.size({height: 100, width: "100%"})
}.margin(10)
.borderRadius(10)
.backgroundColor("#FFFFFFFF")
}, (item: string) => item)
.onMove((from:number, to:number) => {
let tmp = this.arr.splice(from, 1);
this.arr.splice(to, 0, tmp[0])
})
}
.width('100%')
.height('100%')
.backgroundColor("#FFDCDCDC")
}
}
aboutToAppear(): void {
for (let i = 0; i < 100; i++) {
this.arr.push(i.toString())
}
}
}
使用建议
- ArkUI中ForEach的keyGenerator默认是包含索引值的,但是在实际开发中,推荐尽量避免在最终的键值生成规则中包含数据项的索引,以防出现渲染结果非预期和渲染性能降低的问题。
- 为满足键值的唯一性,建议使用对象数据中的唯一id作为键值。
- 基本数据类型的数据项没有唯一ID属性,建议保证数据源不重复或者将其转化为带有唯一ID的对象类型来使用。
不推荐案例
渲染结果非预期
@Entry
@Component
struct Parent {
@State simpleList: Array<string> = ['one', 'two', 'three'];
build() {
Column() {
Button() {
Text('在第1项后插入新项').fontSize(30)
}
.onClick(() => {
this.simpleList.splice(1, 0, 'new item');
})
ForEach(this.simpleList, (item: string) => {
ChildItem({ item: item })
}, (item: string, index: number) => index.toString())
}
.justifyContent(FlexAlign.Center)
.width('100%')
.height('100%')
.backgroundColor(0xF1F3F5)
}
}
@Component
struct ChildItem {
@Prop item: string;
build() {
Text(this.item)
.fontSize(30)
}
}
上述代码的初始渲染效果和点击“在第1项后插入新项”文本组件后的渲染效果如下图所示。
ForEach在首次渲染时,创建的键值依次为"0"、“1”、“2”。
插入新项后,数据源simpleList变为[‘one’, ‘new item’, ‘two’, ‘three’],框架监听到@State装饰的数据源长度变化触发ForEach重新渲染。
ForEach依次遍历新数据源,遍历数据项"one"时生成键值"0",存在相同键值,因此不创建新组件。继续遍历数据项"new item"时生成键值"1",存在相同键值,因此不创建新组件。继续遍历数据项"two"生成键值"2",存在相同键值,因此不创建新组件。最后遍历数据项"three"时生成键值"3",不存在相同键值,创建内容为"three"的新组件并渲染。
从以上可以看出,当最终键值生成规则包含index时,期望的界面渲染结果为[‘one’, ‘new item’, ‘two’, ‘three’],而实际的渲染结果为[‘one’, ‘two’, ‘three’, ‘three’],渲染结果不符合开发者预期。因此,开发者在使用ForEach时应尽量避免最终键值生成规则中包含index。
渲染性能降低
@Entry
@Component
struct Parent {
@State simpleList: Array<string> = ['one', 'two', 'three'];
build() {
Column() {
Button() {
Text('在第1项后插入新项').fontSize(30)
}
.onClick(() => {
this.simpleList.splice(1, 0, 'new item');
console.log(`[onClick]: simpleList is ${JSON.stringify(this.simpleList)}`);
})
ForEach(this.simpleList, (item: string) => {
ChildItem({ item: item })
})
}
.justifyContent(FlexAlign.Center)
.width('100%')
.height('100%')
.backgroundColor(0xF1F3F5)
}
}
@Component
struct ChildItem {
@Prop item: string;
aboutToAppear() {
console.log(`[aboutToAppear]: item is ${this.item}`);
}
build() {
Text(this.item)
.fontSize(50)
}
}
以上代码的初始渲染效果和点击"在第1项后插入新项"文本组件后的渲染效果如下图所示。
点击“在第1项后插入新项”文本组件后,IDE的日志打印结果如下所示。
插入新项后,ForEach为new item、 two、 three三个数组项创建了对应的组件ChildItem,并执行了组件的aboutToAppear()生命周期函数。这是因为:
在ForEach首次渲染时,创建的键值依次为0__one、1__two、2__three。
插入新项后,数据源simpleList变为[‘one’, ‘new item’, ‘two’, ‘three’],ArkUI框架监听到@State装饰的数据源长度变化触发ForEach重新渲染。
ForEach依次遍历新数据源,遍历数据项one时生成键值0__one,键值已存在,因此不创建新组件。继续遍历数据项new item时生成键值1__new item,不存在相同键值,创建内容为new item的新组件并渲染。继续遍历数据项two生成键值2__two,不存在相同键值,创建内容为two的新组件并渲染。最后遍历数据项three时生成键值3__three,不存在相同键值,创建内容为three的新组件并渲染。
尽管此示例中界面渲染的结果符合预期,但每次插入一条新数组项时,ForEach都会为从该数组项起后面的所有数组项全部重新创建组件。当数据源数据量较大或组件结构复杂时,由于组件无法得到复用,将导致性能体验不佳。因此,除非必要,否则不推荐将第三个参数KeyGenerator函数处于缺省状态,以及在键值生成规则中包含数据项索引index。