往期鸿蒙全套实战文章必看:(文中附带鸿蒙全栈学习资料)
- 鸿蒙开辟核心知识点,看这篇文章就够了
- 最新版!鸿蒙HarmonyOS Next应用开辟实战学习蹊径
- 鸿蒙HarmonyOS NEXT开辟技能最全学习蹊径指南
- 鸿蒙应用开辟实战项目,看这一篇文章就够了(部门项目附源码)
ForEach:循环渲染
ForEach接口基于数组范例数据来举行循环渲染,须要与容器组件共同利用,且接口返回的组件应当是允许包罗在ForEach父容器组件中的子组件。
分析
从API version 9开始,该接口支持在ArkTS卡片中利用。
键值天生规则
在ForEach循环渲染过程中,体系会为每个数组元素天生一个唯一且长期的键值,用于标识对应的组件。当这个键值变革时,ArkUI框架将视为该数组元素已被更换或修改,并会基于新的键值创建一个新的组件。
ForEach提供了一个名为keyGenerator的参数,这是一个函数,开辟者可以通过它自界说键值的天生规则。假如开辟者没有界说keyGenerator函数,则ArkUI框架会利用默认的键值天生函数,即(item: Object, index: number) => { return index + '__' + JSON.stringify(item); }。
ArkUI框架对于ForEach的键值天生有一套特定的判定规则,这重要与itemGenerator函数的第二个参数index以及keyGenerator函数的第二个参数index有关,具体的键值天生规则判定逻辑如下图所示。
图1 ForEach键值天生规则
分析
ArkUI框架会对重复的键值发出告诫。在UI更新的场景下,假如出现重复的键值,框架大概无法正常工作。
组件创建规则
在确定键值天生规则后,ForEach的第二个参数itemGenerator函数会根据键值天生规则为数据源的每个数组项创建组件。
初次渲染
在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: number) => {
- ArticleSkeletonView()
- .margin({ top: 20 })
- }, (item: number) => item.toString())
- }
- .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() {
- // 此处'app.media.icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
- 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.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() {
- // 此处'app.media.icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
- 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() {
- // 此处app.media.iconLiked','app.media.iconUnLiked'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
- 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)
- }
- }
复制代码 上述代码的初始运行结果(左图)和点击第1个文章卡片上的点赞图标后的运行结果(右图)如下图所示。
图7 数据源数组项子属性变革案例运行结果图
在本示例中,Article类被@Observed装饰器修饰。父组件ArticleListView传入Article对象实例给子组件ArticleCard,子组件利用@ObjectLink装饰器吸取该实例。
- 当点击第1个文章卡片上的点赞图标时,会触发ArticleCard组件的handleLiked函数。该函数修改第1个卡片对应组件里article实例的isLiked和likesCount属性值。
- 由于子组件ArticleCard中的article利用了@ObjectLink装饰器,父子组件共享同一份article数据。因此,父组件中articleList的第1个数组项的isLiked和likedCounts数值也会同步修改。
- 当父组件监听到数据源数组项属性值变革时,会触发ForEach重新渲染。
- 在此处,ForEach键值天生规则为数组项的id属性值。当ForEach遍历新数据源时,数组项的id均没有变革,不会新建组件。
- 渲染第1个数组项对应的ArticleCard组件时,读取到的isLiked和likesCount为修改后的新值。
拖拽排序
当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())
- }
- }
- }
复制代码 图8 ForEach拖拽排序结果图
利用发起
- 为满意键值的唯一性,对于对象数据范例,发起利用对象数据中的唯一id作为键值。
- 只管克制在终极的键值天生规则中包罗数据项索引index,以防止出现渲染结果非预期和渲染性能低沉。假如业务确实须要利用index,比方列表须要通过index举行条件渲染,开辟者须要担当ForEach在改变数据源后重新创建组件所带来的性能斲丧。
- 根本数据范例的数据项没有唯一ID属性。假如利用根本数据范例自己作为键值,必须确保数组项无重复。因此,对于数据源会发生变革的场景,发起将根本数据范例数组转化为具备唯一ID属性的对象数据范例数组,再利用ID属性作为键值天生规则。
- 对于以上限定规则,index参数存在的意义为:index是开辟者包管键值唯一性的终极本领;对数据项举行修改时,由于itemGenerator中的item参数是不可修改的,以是须用index索引值对数据源举行修改,进而触发UI重新渲染。
- ForEach在下列容器组件 List、Grid、Swiper以及WaterFlow 内利用的时间,不要与LazyForEach 混用。 以List为例,同时包罗ForEach、LazyForEach的环境是不保举的。
不保举案例
开辟者在利用ForEach的过程中,若对于键值天生规则的明白不敷充实,大概会出现错误的利用方式。错误利用一方面会导致功能层面题目,比方渲染结果非预期,另一方面会导致性能层面题目,比方渲染性能低沉。
渲染结果非预期
在本示例中,通过设置ForEach的第三个参数KeyGenerator函数,自界说键值天生规则为数据源的索引index的字符串范例值。当点击父组件Parent中“在第1项后插入新项”文本组件后,界面会出现非预期的结果。
- @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项后插入新项”文本组件后的渲染结果如下图所示。
图9 渲染结果非预期运行结果图
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。
渲染性能低沉
在本示例中,ForEach的第三个参数KeyGenerator函数处于缺省状态。根据上述键值天生规则,此例利用框架默认的键值天生规则,即终极键值为字符串index + '__' + JSON.stringify(item)。当点击“在第1项后插入新项”文本组件后,ForEach将须要为第2个数组项以及厥后的全部项重新创建组件。
- @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项后插入新项"文本组件后的渲染结果如下图所示。
图10 渲染性能低沉案例运行结果图
点击“在第1项后插入新项”文本组件后,IDE的日记打印结果如下所示。
图11 渲染性能低沉案例日记打印图
插入新项后,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。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |