IT评测·应用市场-qidao123.com

标题: HarmonyOS实战开辟:ForEach:循环渲染 [打印本页]

作者: 杀鸡焉用牛刀    时间: 2024-6-13 21:25
标题: HarmonyOS实战开辟:ForEach:循环渲染
ForEach接口基于数组类型数据来举行循环渲染,必要与容器组件配合使用,且接口返回的组件应当是允许包含在ForEach父容器组件中的子组件。比方,ListItem组件要求ForEach的父容器组件必须为List组件。
   说明:
  从API version 9开始,该接口支持在ArkTS卡片中使用。
  接口形貌

  1. ForEach(
  2.   arr: Array,
  3.   itemGenerator: (item: any, index?: number) => void,
  4.   keyGenerator?: (item: any, index?: number): string => string
  5. )
复制代码
以下是参数的详细说明:
参数名参数类型是否必填参数形貌arrArray是数据源,为类型的数组。
说明:
- 可以设置为空数组,此时不会创建子组件。
- 可以设置返回值为数组类型的函数,比方,但设置的函数不应改变包括数组本身在内的任何状态变量,比方不应使用,或这些会改变原数组的函数。Arrayarr.slice(1, 3)Array.splice()Array.sort()Array.reverse()itemGenerator(item: any, index?: number) => void是组件天生函数。
- 为数组中的每个元素创建对应的组件。
- 参数:数组中的数据项。
- 参数(可选):数组中的数据项索引。
说明:
- 组件的类型必须是的父容器所允许的。比方,组件要求的父容器组件必须为组件。itemarrindexarrForEachListItemForEachListkeyGenerator(item: any, index?: number) => string否键值天生函数。
- 为数据源的每个数组项天生唯一且持久的键值。函数返回值为开辟者自定义的键值天生规则。
- 参数:数组中的数据项。
- 参数(可选):数组中的数据项索引。
说明:
- 如果函数缺省,框架默认的键值天生函数为
- 键值天生函数不应改变任何组件状态。arritemarrindexarr(item: T, index: number) => { return index + '__' + JSON.stringify(item); }   说明:
  
  键值天生规则

在循环渲染过程中,系统会为每个数组元素天生一个唯一且持久的键值,用于标识对应的组件。当这个键值变革时,ArkUI框架将视为该数组元素已被更换或修改,并会基于新的键值创建一个新的组件。ForEach
ForEach提供了一个名为的参数,这是一个函数,开辟者可以通过它自定义键值的天生规则。如果开辟者没有定义函数,则ArkUI框架会使用默认的键值天生函数,即。keyGeneratorkeyGenerator(item: any, index: number) => { return index + '__' + JSON.stringify(item); }
ArkUI框架对于的键值天生有一套特定的判断规则,这主要与函数的第二个参数以及函数的返回值有关。总的来说,只有当开辟者在函数中声明了参数,并且自定义的函数返回值中不包含参数时,ArkUI框架才会在开辟者自定义的函数返回值前添加参数,作为终极的键值。在其他情况下,系统将直接使用开辟者自定义的函数返回值作为终极的键值。如果函数未定义,系统将使用上述默认的键值天生函数。详细的键值天生规则判断逻辑如下图所示。ForEachitemGeneratorindexkeyGeneratoritemGeneratorindexkeyGeneratorindexkeyGeneratorindexkeyGeneratorkeyGenerator
图1 ForEach键值天生规则

   说明:
  ArkUI框架会对重复的键值发出告诫。在UI更新的场景下,如果出现重复的键值,框架可能无法正常工作,详细请参见渲染结果非预期。
  组件创建规则

在确定键值天生规则后,ForEach的第二个参数函数会根据键值天生规则为数据源的每个数组项创建组件。组件的创建包括两种情况:ForEach首次渲染和ForEach非首次渲染。itemGenerator
首次渲染

在ForEach首次渲染时,会根据前述键值天生规则为数据源的每个数组项天生唯一键值,并创建相应的组件。
  1. @Entry
  2. @Component
  3. struct Parent {
  4.   @State simpleList: Array<string> = ['one', 'two', 'three'];
  5.   build() {
  6.     Row() {
  7.       Column() {
  8.         ForEach(this.simpleList, (item: string) => {
  9.           ChildItem({ item: item })
  10.         }, (item: string) => item)
  11.       }
  12.       .width('100%')
  13.       .height('100%')
  14.     }
  15.     .height('100%')
  16.     .backgroundColor(0xF1F3F5)
  17.   }
  18. }
  19. @Component
  20. struct ChildItem {
  21.   @Prop item: string;
  22.   build() {
  23.     Text(this.item)
  24.       .fontSize(50)
  25.   }
  26. }
复制代码
运行结果如下图所示。
图2 ForEach数据源不存在雷同值案例首次渲染运行结果图

在上述代码中,键值天生规则是函数的返回值。在ForEach渲染循环时,为数据源数组项依次天生键值、和,并创建对应的组件渲染到界面上。keyGeneratoritemonetwothreeChildItem
当不同数组项按照键值天生规则天生的键值雷同时,框架的行为是未定义的。比方,在以下代码中,ForEach渲染雷同的数据项时,只创建了一个组件,而没有创建多个具有雷同键值的组件。twoChildItem
  1. @Entry
  2. @Component
  3. struct Parent {
  4.   @State simpleList: Array<string> = ['one', 'two', 'two', 'three'];
  5.   build() {
  6.     Row() {
  7.       Column() {
  8.         ForEach(this.simpleList, (item: string) => {
  9.           ChildItem({ item: item })
  10.         }, (item: string) => item)
  11.       }
  12.       .width('100%')
  13.       .height('100%')
  14.     }
  15.     .height('100%')
  16.     .backgroundColor(0xF1F3F5)
  17.   }
  18. }
  19. @Component
  20. struct ChildItem {
  21.   @Prop item: string;
  22.   build() {
  23.     Text(this.item)
  24.       .fontSize(50)
  25.   }
  26. }
复制代码
运行结果如下图所示。
图3 ForEach数据源存在雷同值案例首次渲染运行结果图

在该示例中,终极键值天生规则为。当ForEach遍历数据源,遍历到索引为1的时,按照终极键值天生规则天生键值为的组件并举行标记。当遍历到索引为2的时,按照终极键值天生规则当前项的键值也为,此时不再创建新的组件。itemsimpleListtwotwotwotwo
非首次渲染

在ForEach组件举行非首次渲染时,它会查抄新天生的键值是否在上次渲染中已经存在。如果键值不存在,则会创建一个新的组件;如果键值存在,则不会创建新的组件,而是直接渲染该键值所对应的组件。比方,在以下的代码示例中,通过点击事件修改了数组的第三项值为"new three",这将触发ForEach组件举行非首次渲染。
  1. @Entry
  2. @Component
  3. struct Parent {
  4.   @State simpleList: Array<string> = ['one', 'two', 'three'];
  5.   build() {
  6.     Row() {
  7.       Column() {
  8.         Text('点击修改第3个数组项的值')
  9.           .fontSize(24)
  10.           .fontColor(Color.Red)
  11.           .onClick(() => {
  12.             this.simpleList[2] = 'new three';
  13.           })
  14.         ForEach(this.simpleList, (item: string) => {
  15.           ChildItem({ item: item })
  16.             .margin({ top: 20 })
  17.         }, (item: string) => item)
  18.       }
  19.       .justifyContent(FlexAlign.Center)
  20.       .width('100%')
  21.       .height('100%')
  22.     }
  23.     .height('100%')
  24.     .backgroundColor(0xF1F3F5)
  25.   }
  26. }
  27. @Component
  28. struct ChildItem {
  29.   @Prop item: string;
  30.   build() {
  31.     Text(this.item)
  32.       .fontSize(30)
  33.   }
  34. }
复制代码
运行结果如下图所示。
图4 ForEach非首次渲染案例运行结果图

从本例可以看出 能够监听到简单数据类型数组数据源 数组项的变革。@StatesimpleList
使用场景

ForEach组件在开辟过程中的主要应用场景包括:数据源稳定、数据源数组项发生变革(如插入、删除利用)、数据源数组项子属性变革。
数据源稳定

在数据源保持稳定的场景中,数据源可以直接采用根本数据类型。比方,在页面加载状态时,可以使用骨架屏列表举行渲染展示。
  1. @Entry
  2. @Component
  3. struct ArticleList {
  4.   @State simpleList: Array<number> = [1, 2, 3, 4, 5];
  5.   build() {
  6.     Column() {
  7.       ForEach(this.simpleList, (item: string) => {
  8.         ArticleSkeletonView()
  9.           .margin({ top: 20 })
  10.       }, (item: string) => item)
  11.     }
  12.     .padding(20)
  13.     .width('100%')
  14.     .height('100%')
  15.   }
  16. }
  17. @Builder
  18. function textArea(width: number | Resource | string = '100%', height: number | Resource | string = '100%') {
  19.   Row()
  20.     .width(width)
  21.     .height(height)
  22.     .backgroundColor('#FFF2F3F4')
  23. }
  24. @Component
  25. struct ArticleSkeletonView {
  26.   build() {
  27.     Row() {
  28.       Column() {
  29.         textArea(80, 80)
  30.       }
  31.       .margin({ right: 20 })
  32.       Column() {
  33.         textArea('60%', 20)
  34.         textArea('50%', 20)
  35.       }
  36.       .alignItems(HorizontalAlign.Start)
  37.       .justifyContent(FlexAlign.SpaceAround)
  38.       .height('100%')
  39.     }
  40.     .padding(20)
  41.     .borderRadius(12)
  42.     .backgroundColor('#FFECECEC')
  43.     .height(120)
  44.     .width('100%')
  45.     .justifyContent(FlexAlign.SpaceBetween)
  46.   }
  47. }
复制代码
运行结果如下图所示。
图5 骨架屏运行结果图

在本示例中,采用数据项item作为键值天生规则,由于数据源simpleList的数组项各不雷同,因此能够保证键值的唯一性。
数据源数组项发生变革

在数据源数组项发生变革的场景下,比方举行数组插入、删除利用大概数组项索引位置发生互换时,数据源应为对象数组类型,并使用对象的唯一ID作为终极键值。比方,当在页面上通过手势上滑加载下一页数据时,会在数据源数组尾部新增新获取的数据项,从而使得数据源数组长度增大。
  1. class Article {
  2.   id: string;
  3.   title: string;
  4.   brief: string;
  5.   constructor(id: string, title: string, brief: string) {
  6.     this.id = id;
  7.     this.title = title;
  8.     this.brief = brief;
  9.   }
  10. }
  11. @Entry
  12. @Component
  13. struct ArticleListView {
  14.   @State isListReachEnd: boolean = false;
  15.   @State articleList: Array<Article> = [
  16.     new Article('001', '第1篇文章', '文章简介内容'),
  17.     new Article('002', '第2篇文章', '文章简介内容'),
  18.     new Article('003', '第3篇文章', '文章简介内容'),
  19.     new Article('004', '第4篇文章', '文章简介内容'),
  20.     new Article('005', '第5篇文章', '文章简介内容'),
  21.     new Article('006', '第6篇文章', '文章简介内容')
  22.   ]
  23.   loadMoreArticles() {
  24.     this.articleList.push(new Article('007', '加载的新文章', '文章简介内容'));
  25.   }
  26.   build() {
  27.     Column({ space: 5 }) {
  28.       List() {
  29.         ForEach(this.articleList, (item: Article) => {
  30.           ListItem() {
  31.             ArticleCard({ article: item })
  32.               .margin({ top: 20 })
  33.           }
  34.         }, (item: Article) => item.id)
  35.       }
  36.       .onReachEnd(() => {
  37.         this.isListReachEnd = true;
  38.       })
  39.       .parallelGesture(
  40.         PanGesture({ direction: PanDirection.Up, distance: 80 })
  41.           .onActionStart(() => {
  42.             if (this.isListReachEnd) {
  43.               this.loadMoreArticles();
  44.               this.isListReachEnd = false;
  45.             }
  46.           })
  47.       )
  48.       .padding(20)
  49.       .scrollBar(BarState.Off)
  50.     }
  51.     .width('100%')
  52.     .height('100%')
  53.     .backgroundColor(0xF1F3F5)
  54.   }
  55. }
  56. @Component
  57. struct ArticleCard {
  58.   @Prop article: Article;
  59.   build() {
  60.     Row() {
  61.       Image($r('app.media.icon'))
  62.         .width(80)
  63.         .height(80)
  64.         .margin({ right: 20 })
  65.       Column() {
  66.         Text(this.article.title)
  67.           .fontSize(20)
  68.           .margin({ bottom: 8 })
  69.         Text(this.article.brief)
  70.           .fontSize(16)
  71.           .fontColor(Color.Gray)
  72.           .margin({ bottom: 8 })
  73.       }
  74.       .alignItems(HorizontalAlign.Start)
  75.       .width('80%')
  76.       .height('100%')
  77.     }
  78.     .padding(20)
  79.     .borderRadius(12)
  80.     .backgroundColor('#FFECECEC')
  81.     .height(120)
  82.     .width('100%')
  83.     .justifyContent(FlexAlign.SpaceBetween)
  84.   }
  85. }
复制代码
初始运行结果(左图)和手势上滑加载后结果(右图)如下图所示。
图6 数据源数组项变革案例运行结果图

在本示例中,组件作为组件的子组件,通过装饰器接收一个对象,用于渲染文章卡片。ArticleCardArticleListView@PropArticle
数据源数组项子属性变革

当数据源的数组项为对象数据类型,并且只修改某个数组项的属性值时,由于数据源为复杂数据类型,ArkUI框架无法监听到装饰器修饰的数据源数组项的属性变革,从而无法触发的重新渲染。为实现重新渲染,必要结合和装饰器使用。比方,在文章列表卡片上点击“点赞”按钮,从而修改文章的点赞数目。@StateForEachForEach@Observed@ObjectLink
  1. @Observed
  2. class Article {
  3.   id: string;
  4.   title: string;
  5.   brief: string;
  6.   isLiked: boolean;
  7.   likesCount: number;
  8.   constructor(id: string, title: string, brief: string, isLiked: boolean, likesCount: number) {
  9.     this.id = id;
  10.     this.title = title;
  11.     this.brief = brief;
  12.     this.isLiked = isLiked;
  13.     this.likesCount = likesCount;
  14.   }
  15. }
  16. @Entry
  17. @Component
  18. struct ArticleListView {
  19.   @State articleList: Array<Article> = [
  20.     new Article('001', '第0篇文章', '文章简介内容', false, 100),
  21.     new Article('002', '第1篇文章', '文章简介内容', false, 100),
  22.     new Article('003', '第2篇文章', '文章简介内容', false, 100),
  23.     new Article('004', '第4篇文章', '文章简介内容', false, 100),
  24.     new Article('005', '第5篇文章', '文章简介内容', false, 100),
  25.     new Article('006', '第6篇文章', '文章简介内容', false, 100),
  26.   ];
  27.   build() {
  28.     List() {
  29.       ForEach(this.articleList, (item: Article) => {
  30.         ListItem() {
  31.           ArticleCard({
  32.             article: item
  33.           })
  34.             .margin({ top: 20 })
  35.         }
  36.       }, (item: Article) => item.id)
  37.     }
  38.     .padding(20)
  39.     .scrollBar(BarState.Off)
  40.     .backgroundColor(0xF1F3F5)
  41.   }
  42. }
  43. @Component
  44. struct ArticleCard {
  45.   @ObjectLink article: Article;
  46.   handleLiked() {
  47.     this.article.isLiked = !this.article.isLiked;
  48.     this.article.likesCount = this.article.isLiked ? this.article.likesCount + 1 : this.article.likesCount - 1;
  49.   }
  50.   build() {
  51.     Row() {
  52.       Image($r('app.media.icon'))
  53.         .width(80)
  54.         .height(80)
  55.         .margin({ right: 20 })
  56.       Column() {
  57.         Text(this.article.title)
  58.           .fontSize(20)
  59.           .margin({ bottom: 8 })
  60.         Text(this.article.brief)
  61.           .fontSize(16)
  62.           .fontColor(Color.Gray)
  63.           .margin({ bottom: 8 })
  64.         Row() {
  65.           Image(this.article.isLiked ? $r('app.media.iconLiked') : $r('app.media.iconUnLiked'))
  66.             .width(24)
  67.             .height(24)
  68.             .margin({ right: 8 })
  69.           Text(this.article.likesCount.toString())
  70.             .fontSize(16)
  71.         }
  72.         .onClick(() => this.handleLiked())
  73.         .justifyContent(FlexAlign.Center)
  74.       }
  75.       .alignItems(HorizontalAlign.Start)
  76.       .width('80%')
  77.       .height('100%')
  78.     }
  79.     .padding(20)
  80.     .borderRadius(12)
  81.     .backgroundColor('#FFECECEC')
  82.     .height(120)
  83.     .width('100%')
  84.     .justifyContent(FlexAlign.SpaceBetween)
  85.   }
  86. }
复制代码
上述代码的初始运行结果(左图)和点击第1个文章卡片上的点赞图标后的运行结果(右图)如下图所示。
图7 数据源数组项子属性变革案例运行结果图

在本示例中,类被装饰器修饰。父组件传入对象实例给子组件,子组件使用装饰器接收该实例。Article@ObservedArticleListViewArticleArticleCard@ObjectLink
使用建议


不保举案例

开辟者在使用ForEach的过程中,若对于键值天生规则的理解不够充分,可能会出现错误的使用方式。错误使用一方面会导致功能层面问题,比方渲染结果非预期,另一方面会导致性能层面问题,比方渲染性能降低。
渲染结果非预期

在本示例中,通过设置的第三个参数函数,自定义键值天生规则为数据源的索引的字符串类型值。当点击父组件中“在第1项后插入新项”文本组件后,界面会出现非预期的结果。ForEachKeyGeneratorindexParent
  1. @Entry
  2. @Component
  3. struct Parent {
  4.   @State simpleList: Array<string> = ['one', 'two', 'three'];
  5.   build() {
  6.     Column() {
  7.       Button() {
  8.         Text('在第1项后插入新项').fontSize(30)
  9.       }
  10.       .onClick(() => {
  11.         this.simpleList.splice(1, 0, 'new item');
  12.       })
  13.       ForEach(this.simpleList, (item: string) => {
  14.         ChildItem({ item: item })
  15.       }, (item: string, index: number) => index.toString())
  16.     }
  17.     .justifyContent(FlexAlign.Center)
  18.     .width('100%')
  19.     .height('100%')
  20.     .backgroundColor(0xF1F3F5)
  21.   }
  22. }
  23. @Component
  24. struct ChildItem {
  25.   @Prop item: string;
  26.   build() {
  27.     Text(this.item)
  28.       .fontSize(30)
  29.   }
  30. }
复制代码
上述代码的初始渲染结果(左图)和点击“在第1项后插入新项”文本组件后的渲染结果(右图)如下图所示。
图8 渲染结果非预期运行结果图

ForEach在首次渲染时,创建的键值依次为"0"、"1"、"2"。
插入新项后,数据源变为,框架监听到装饰的数据源长度变革触发重新渲染。simpleList['one', 'new item', 'two', 'three']@StateForEach
ForEach依次遍历新数据源,遍历数据项"one"时天生键值"0",存在雷同键值,因此不创建新组件。继续遍历数据项"new item"时天生键值"1",存在雷同键值,因此不创建新组件。继续遍历数据项"two"天生键值"2",存在雷同键值,因此不创建新组件。最后遍历数据项"three"时天生键值"3",不存在雷同键值,创建内容为"three"的新组件并渲染。
从以上可以看出,当终极键值天生规则包含时,渴望的界面渲染结果为,而现实的渲染结果为,渲染结果不符合开辟者预期。因此,开辟者在使用时应只管制止终极键值天生规则中包含。index['one', 'new item', 'two', 'three']['one', 'two', 'three', 'three']ForEachindex
渲染性能降低

在本示例中,的第三个参数函数处于缺省状态。根据上述键值天生规则,此例使用框架默认的键值天生规则,即终极键值为字符串。当点击“在第1项后插入新项”文本组件后,将必要为第2个数组项以及其后的全部项重新创建组件。ForEachKeyGeneratorindex + '__' + JSON.stringify(item)ForEach
  1. @Entry
  2. @Component
  3. struct Parent {
  4.   @State simpleList: Array<string> = ['one', 'two', 'three'];
  5.   build() {
  6.     Column() {
  7.       Button() {
  8.         Text('在第1项后插入新项').fontSize(30)
  9.       }
  10.       .onClick(() => {
  11.         this.simpleList.splice(1, 0, 'new item');
  12.         console.log(`[onClick]: simpleList is ${JSON.stringify(this.simpleList)}`);
  13.       })
  14.       ForEach(this.simpleList, (item: string) => {
  15.         ChildItem({ item: item })
  16.       })
  17.     }
  18.     .justifyContent(FlexAlign.Center)
  19.     .width('100%')
  20.     .height('100%')
  21.     .backgroundColor(0xF1F3F5)
  22.   }
  23. }
  24. @Component
  25. struct ChildItem {
  26.   @Prop item: string;
  27.   aboutToAppear() {
  28.     console.log(`[aboutToAppear]: item is ${this.item}`);
  29.   }
  30.   build() {
  31.     Text(this.item)
  32.       .fontSize(50)
  33.   }
  34. }
复制代码
以上代码的初始渲染结果(左图)和点击"在第1项后插入新项"文本组件后的渲染结果(右图)如下所示。
图9 渲染性能降低案例运行结果图

点击“在第1项后插入新项”文本组件后,IDE的日记打印结果如下所示。
图10 渲染性能降低案例日记打印图

插入新项后,为、 、 三个数组项创建了对应的组件,并执行了组件的aboutToAppear()生命周期函数。这是因为:ForEachnew itemtwothreeChildItem
只管此示例中界面渲染的结果符合预期,但每次插入一条新数组项时,都会为从该数组项起背面的全部数组项全部重新创建组件。当数据源数据量较大或组件结构复杂时,由于组件无法得到复用,将导致性能体验不佳。因此,除非必要,否则不保举将第三个参数函数处于缺省状态,以及在键值天生规则中包含数据项索引。ForEachKeyGeneratorindex

最后

有许多小同伴不知道学习哪些鸿蒙开辟技术?不知道必要重点把握哪些鸿蒙应用开辟知识点?而且学习时频繁踩坑,终极浪费大量时间。所以有一份实用的鸿蒙(HarmonyOS NEXT)资料用来跟着学习是非常有必要的。 
这份鸿蒙(HarmonyOS NEXT)资料包含了鸿蒙开辟必把握的核心知识要点,内容包含了ArkTS、ArkUI开辟组件、Stage模子、多端摆设、分布式应用开辟、音频、视频、WebGL、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、Harmony南向开辟、鸿蒙项目实战等等)鸿蒙(HarmonyOS NEXT)技术知识点。
希望这一份鸿蒙学习资料能够给各人带来帮助,有必要的小同伴自行领取,限时开源,先到先得~无套路领取!!
获取这份完备版高清学习路线,请点击→纯血版全套鸿蒙HarmonyOS学习资料
鸿蒙(HarmonyOS NEXT)最新学习路线












有了路线图,怎么能没有学习资料呢,小编也准备了一份团结鸿蒙官方发布笔记整理收纳的一套系统性的鸿蒙(OpenHarmony )学习手册(共计1236页)鸿蒙(OpenHarmony )开辟入门教学视频,内容包含:ArkTS、ArkUI、Web开辟、应用模子、资源分类…等知识点。
获取以上完备版高清学习路线,请点击→纯血版全套鸿蒙HarmonyOS学习资料
《鸿蒙 (OpenHarmony)开辟入门教学视频》


《鸿蒙生态应用开辟V2.0白皮书》


《鸿蒙 (OpenHarmony)开辟基础到实战手册》

OpenHarmony北向、南向开辟环境搭建

 《鸿蒙开辟基础》



 《鸿蒙开辟进阶》



《鸿蒙进阶实战》



 获取以上完备鸿蒙HarmonyOS学习资料,请点击→纯血版全套鸿蒙HarmonyOS学习资料
总结

总的来说,华为鸿蒙不再兼容安卓,对中年程序员来说是一个挑战,也是一个机会。只有积极应对变革,不断学习和提升自己,他们才能在这个变革的期间中立于不败之地。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 IT评测·应用市场-qidao123.com (https://dis.qidao123.com/) Powered by Discuz! X3.4