• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# ForEach:循环渲染
2<!--Kit: ArkUI-->
3<!--Subsystem: ArkUI-->
4<!--Owner: @maorh-->
5<!--Designer: @keerecles-->
6<!--Tester: @TerryTsao-->
7<!--Adviser: @zhang_yixin13-->
8
9ForEach接口基于数组循环渲染,需要与容器组件配合使用,且接口返回的组件应当是允许包含在ForEach父容器组件中的子组件。例如,ListItem组件要求ForEach的父容器组件必须为[List组件](../../reference/apis-arkui/arkui-ts/ts-container-list.md)。
10
11API参数说明见:[ForEach API参数说明](../../reference/apis-arkui/arkui-ts/ts-rendering-control-foreach.md)。
12
13> **说明:**
14>
15> 从API version 9开始,该接口支持在ArkTS卡片中使用。
16
17## 键值生成规则
18
19在`ForEach`循环渲染过程中,系统会为每个数组元素生成一个唯一且持久的键值,用于标识对应的组件。当键值变化时,ArkUI框架会视为该数组元素已被替换或修改,并会基于新的键值创建一个新的组件。
20
21`ForEach`提供了一个名为`keyGenerator`的参数,这是一个函数,开发者可以通过它自定义键值的生成规则。如果开发者没有定义`keyGenerator`函数,则ArkUI框架会使用默认的键值生成函数,即`(item: Object, index: number) => { return index + '__' + JSON.stringify(item); }`。
22
23ArkUI框架对于`ForEach`的键值生成有一套特定的判断规则,这主要与`itemGenerator`函数和`keyGenerator`函数的第二个参数`index`有关。具体的键值生成规则判断逻辑如下图所示。
24
25**图1** ForEach键值生成规则
26![ForEach-Key-Generation-Rules](figures/ForEach-Key-Generation-Rules.png)
27
28> **说明:**
29>
30> ArkUI框架会对重复的键值发出警告。在UI更新时,如果出现重复的键值,框架可能无法正常工作,具体请参见[渲染结果非预期](#渲染结果非预期)。
31
32## 组件创建规则
33
34在确定键值生成规则后,ForEach的第二个参数`itemGenerator`函数会根据键值生成规则为数据源的每个数组项创建组件。组件的创建包括两种情况:[ForEach首次渲染](#首次渲染)和[ForEach非首次渲染](#非首次渲染)。
35
36### 首次渲染
37
38在ForEach首次渲染时,会根据前述键值生成规则为数据源的每个数组项生成唯一键值,并创建相应的组件。
39
40```ts
41@Entry
42@Component
43struct Parent {
44  @State simpleList: Array<string> = ['one', 'two', 'three'];
45
46  build() {
47    Row() {
48      Column() {
49        ForEach(this.simpleList, (item: string) => {
50          ChildItem({ item: item })
51        }, (item: string) => item)
52      }
53      .width('100%')
54      .height('100%')
55    }
56    .height('100%')
57    .backgroundColor(0xF1F3F5)
58  }
59}
60
61@Component
62struct ChildItem {
63  @Prop item: string;
64
65  build() {
66    Text(this.item)
67      .fontSize(50)
68  }
69}
70```
71
72运行效果如下图所示。
73
74**图2**  ForEach数据源不存在相同值案例首次渲染运行效果图
75![ForEach-CaseStudy-1stRender-NoDup](figures/ForEach-CaseStudy-1stRender-NoDup.png)
76
77在上述代码中,`keyGenerator`函数的返回值是`item`。在ForEach渲染循环时,为数组项依次生成键值`one`、`two`和`three`,并创建对应的`ChildItem`组件渲染到界面上。
78
79当不同数组项生成的键值相同时,框架的行为是未定义的。例如,在以下代码中,ForEach渲染相同的数据项`two`时,只创建了一个`ChildItem`组件,而没有创建多个具有相同键值的组件。
80
81```ts
82@Entry
83@Component
84struct Parent {
85  @State simpleList: Array<string> = ['one', 'two', 'two', 'three'];
86
87  build() {
88    Row() {
89      Column() {
90        ForEach(this.simpleList, (item: string) => {
91          ChildItem({ item: item })
92        }, (item: string) => item)
93      }
94      .width('100%')
95      .height('100%')
96    }
97    .height('100%')
98    .backgroundColor(0xF1F3F5)
99  }
100}
101
102@Component
103struct ChildItem {
104  @Prop item: string;
105
106  build() {
107    Text(this.item)
108      .fontSize(50)
109  }
110}
111 ```
112
113运行效果如下图所示。
114
115**图3**  ForEach数据源存在相同值案例首次渲染运行效果图
116![ForEach-CaseStudy-1stRender-Dup](figures/ForEach-CaseStudy-1stRender-Dup.png)
117
118在该示例中,最终键值生成规则为`item`。当ForEach遍历数据源`simpleList`,遍历到索引为1的`two`时,创建键值为`two`的组件并记录。当遍历到索引为2的`two`时,当前项的键值也为`two`,此时不再创建新的组件。
119
120### 非首次渲染
121
122在ForEach组件进行非首次渲染时,它会检查新生成的键值是否在上次渲染中已经存在。如果键值不存在,则会创建一个新的组件;如果键值存在,则不会创建新的组件,而是直接渲染该键值所对应的组件。例如,在以下的代码示例中,通过点击事件修改了数组的第三项值为"new three",这将触发ForEach组件进行非首次渲染。
123
124```ts
125@Entry
126@Component
127struct Parent {
128  @State simpleList: Array<string> = ['one', 'two', 'three'];
129
130  build() {
131    Row() {
132      Column() {
133        Text('点击修改第3个数组项的值')
134          .fontSize(24)
135          .fontColor(Color.Red)
136          .onClick(() => {
137            this.simpleList[2] = 'new three';
138          })
139
140        ForEach(this.simpleList, (item: string) => {
141          ChildItem({ item: item })
142            .margin({ top: 20 })
143        }, (item: string) => item)
144      }
145      .justifyContent(FlexAlign.Center)
146      .width('100%')
147      .height('100%')
148    }
149    .height('100%')
150    .backgroundColor(0xF1F3F5)
151  }
152}
153
154@Component
155struct ChildItem {
156  @Prop item: string;
157
158  build() {
159    Text(this.item)
160      .fontSize(30)
161  }
162}
163```
164
165运行效果如下图所示。
166
167**图4**  ForEach非首次渲染案例运行效果图
168![ForEach-Non-Initial-Render-Case-Effect](figures/ForEach-Non-Initial-Render-Case-Effect.gif)
169
170从本例可以看出[\@State](./arkts-state.md)能够监听到简单数据类型数组`simpleList`数组项的变化。
171
1721. 当`simpleList`数组项发生变化时,会触发`ForEach`重新渲染。
1732. `ForEach`遍历新的数据源`['one', 'two', 'new three']`,并生成对应的键值`one`、`two`和`new three`。
1743. 其中,键值`one`和`two`在上次渲染中已经存在,所以 `ForEach` 复用了对应的组件并进行了渲染。对于第三个数组项 "new three",由于其通过键值生成规则 `item` 生成的键值`new three`在上次渲染中不存在,因此 `ForEach` 为该数组项创建了一个新的组件。
175
176## 使用场景
177
178ForEach组件在开发过程中的主要应用场景包括:[数据源不变](#数据源不变)、[数据源数组项发生变化](#数据源数组项发生变化)(如插入、删除操作)、[数据源数组项子属性变化](#数据源数组项子属性变化)。
179
180### 数据源不变
181
182在数据源保持不变的场景中,数据源可以直接采用基本数据类型。例如,页面加载状态时,可以使用骨架屏列表进行渲染展示。
183
184```ts
185@Entry
186@Component
187struct ArticleList {
188  @State simpleList: Array<number> = [1, 2, 3, 4, 5];
189
190  build() {
191    Column() {
192      ForEach(this.simpleList, (item: number) => {
193        ArticleSkeletonView()
194          .margin({ top: 20 })
195      }, (item: number) => item.toString())
196    }
197    .padding(20)
198    .width('100%')
199    .height('100%')
200  }
201}
202
203@Builder
204function textArea(width: number | Resource | string = '100%', height: number | Resource | string = '100%') {
205  Row()
206    .width(width)
207    .height(height)
208    .backgroundColor('#FFF2F3F4')
209}
210
211@Component
212struct ArticleSkeletonView {
213  build() {
214    Row() {
215      Column() {
216        textArea(80, 80)
217      }
218      .margin({ right: 20 })
219
220      Column() {
221        textArea('60%', 20)
222        textArea('50%', 20)
223      }
224      .alignItems(HorizontalAlign.Start)
225      .justifyContent(FlexAlign.SpaceAround)
226      .height('100%')
227    }
228    .padding(20)
229    .borderRadius(12)
230    .backgroundColor('#FFECECEC')
231    .height(120)
232    .width('100%')
233    .justifyContent(FlexAlign.SpaceBetween)
234  }
235}
236```
237
238运行效果如下图所示。
239
240**图5** 骨架屏运行效果图
241![ForEach-SkeletonScreen](figures/ForEach-SkeletonScreen.png)
242
243在本示例中,采用数据项item作为键值生成规则,由于数据源simpleList的数组项各不相同,因此能够保证键值的唯一性。
244
245### 数据源数组项发生变化
246
247在数据源数组项发生变化的场景下,如数组插入、删除操作或者数组项索引位置交换时,数据源应为对象数组类型,并使用对象的唯一ID作为键值。
248
249```ts
250class Article {
251  id: string;
252  title: string;
253  brief: string;
254
255  constructor(id: string, title: string, brief: string) {
256    this.id = id;
257    this.title = title;
258    this.brief = brief;
259  }
260}
261
262@Entry
263@Component
264struct ArticleListView {
265  @State isListReachEnd: boolean = false;
266  @State articleList: Array<Article> = [
267    new Article('001', '第1篇文章', '文章简介内容'),
268    new Article('002', '第2篇文章', '文章简介内容'),
269    new Article('003', '第3篇文章', '文章简介内容'),
270    new Article('004', '第4篇文章', '文章简介内容'),
271    new Article('005', '第5篇文章', '文章简介内容'),
272    new Article('006', '第6篇文章', '文章简介内容')
273  ];
274
275  loadMoreArticles() {
276    this.articleList.push(new Article('007', '加载的新文章', '文章简介内容'));
277  }
278
279  build() {
280    Column({ space: 5 }) {
281      List() {
282        ForEach(this.articleList, (item: Article) => {
283          ListItem() {
284            ArticleCard({ article: item })
285              .margin({ top: 20 })
286          }
287        }, (item: Article) => item.id)
288      }
289      .onReachEnd(() => {
290        this.isListReachEnd = true;
291      })
292      .parallelGesture(
293        PanGesture({ direction: PanDirection.Up, distance: 80 })
294          .onActionStart(() => {
295            if (this.isListReachEnd) {
296              this.loadMoreArticles();
297              this.isListReachEnd = false;
298            }
299          })
300      )
301      .padding(20)
302      .scrollBar(BarState.Off)
303    }
304    .width('100%')
305    .height('100%')
306    .backgroundColor(0xF1F3F5)
307  }
308}
309
310@Component
311struct ArticleCard {
312  @Prop article: Article;
313
314  build() {
315    Row() {
316      // 此处'app.media.icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
317      Image($r('app.media.icon'))
318        .width(80)
319        .height(80)
320        .margin({ right: 20 })
321
322      Column() {
323        Text(this.article.title)
324          .fontSize(20)
325          .margin({ bottom: 8 })
326        Text(this.article.brief)
327          .fontSize(16)
328          .fontColor(Color.Gray)
329          .margin({ bottom: 8 })
330      }
331      .alignItems(HorizontalAlign.Start)
332      .width('80%')
333      .height('100%')
334    }
335    .padding(20)
336    .borderRadius(12)
337    .backgroundColor('#FFECECEC')
338    .height(120)
339    .width('100%')
340    .justifyContent(FlexAlign.SpaceBetween)
341  }
342}
343```
344
345初始运行效果(左图)和手势上滑加载后效果(右图)如下图所示。
346
347**图6**  数据源数组项变化案例运行效果图
348![ForEach-DataSourceArrayChange](figures/ForEach-DataSourceArrayChange.png)
349
350在本示例中,`ArticleCard`组件作为`ArticleListView`组件的子组件,通过[\@Prop](./arkts-prop.md)装饰器接收一个`Article`对象,用于渲染文章卡片。
351
3521. 当列表滚动到底部且手势滑动距离超过80vp时,触发`loadMoreArticles()`函数。此函数在`articleList`数据源尾部添加新数据项,增加数据源长度。
3532. 数据源被`@State`装饰器修饰,ArkUI框架能够感知数据源长度的变化并触发`ForEach`进行重新渲染。
354
355### 数据源数组项子属性变化
356
357当数据源的数组项为对象数据类型,并且只修改某个数组项的属性值时,由于数据源为复杂数据类型,ArkUI框架无法监听到`@State`装饰器修饰的数据源数组项的属性变化,从而无法触发`ForEach`的重新渲染。为实现`ForEach`重新渲染,需要结合[\@Observed和\@ObjectLink](./arkts-observed-and-objectlink.md)装饰器使用。例如,在文章列表卡片上点击“点赞”按钮,从而修改文章的点赞数量。
358
359```ts
360@Observed
361class Article {
362  id: string;
363  title: string;
364  brief: string;
365  isLiked: boolean;
366  likesCount: number;
367
368  constructor(id: string, title: string, brief: string, isLiked: boolean, likesCount: number) {
369    this.id = id;
370    this.title = title;
371    this.brief = brief;
372    this.isLiked = isLiked;
373    this.likesCount = likesCount;
374  }
375}
376
377@Entry
378@Component
379struct ArticleListView {
380  @State articleList: Array<Article> = [
381    new Article('001', '第0篇文章', '文章简介内容', false, 100),
382    new Article('002', '第1篇文章', '文章简介内容', false, 100),
383    new Article('003', '第2篇文章', '文章简介内容', false, 100),
384    new Article('004', '第4篇文章', '文章简介内容', false, 100),
385    new Article('005', '第5篇文章', '文章简介内容', false, 100),
386    new Article('006', '第6篇文章', '文章简介内容', false, 100),
387  ];
388
389  build() {
390    List() {
391      ForEach(this.articleList, (item: Article) => {
392        ListItem() {
393          ArticleCard({
394            article: item
395          })
396            .margin({ top: 20 })
397        }
398      }, (item: Article) => item.id)
399    }
400    .padding(20)
401    .scrollBar(BarState.Off)
402    .backgroundColor(0xF1F3F5)
403  }
404}
405
406@Component
407struct ArticleCard {
408  @ObjectLink article: Article;
409
410  handleLiked() {
411    this.article.isLiked = !this.article.isLiked;
412    this.article.likesCount = this.article.isLiked ? this.article.likesCount + 1 : this.article.likesCount - 1;
413  }
414
415  build() {
416    Row() {
417      // 此处'app.media.icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
418      Image($r('app.media.icon'))
419        .width(80)
420        .height(80)
421        .margin({ right: 20 })
422
423      Column() {
424        Text(this.article.title)
425          .fontSize(20)
426          .margin({ bottom: 8 })
427        Text(this.article.brief)
428          .fontSize(16)
429          .fontColor(Color.Gray)
430          .margin({ bottom: 8 })
431
432        Row() {
433          // 此处app.media.iconLiked','app.media.iconUnLiked'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
434          Image(this.article.isLiked ? $r('app.media.iconLiked') : $r('app.media.iconUnLiked'))
435            .width(24)
436            .height(24)
437            .margin({ right: 8 })
438          Text(this.article.likesCount.toString())
439            .fontSize(16)
440        }
441        .onClick(() => this.handleLiked())
442        .justifyContent(FlexAlign.Center)
443      }
444      .alignItems(HorizontalAlign.Start)
445      .width('80%')
446      .height('100%')
447    }
448    .padding(20)
449    .borderRadius(12)
450    .backgroundColor('#FFECECEC')
451    .height(120)
452    .width('100%')
453    .justifyContent(FlexAlign.SpaceBetween)
454  }
455}
456```
457
458上述代码的初始运行效果(左图)和点击第1个文章卡片上的点赞图标后的运行效果(右图)如下图所示。
459
460**图7** 数据源数组项子属性变化案例运行效果图
461![ForEach-DataSourceArraySubpropertyChange](figures/ForEach-DataSourceArraySubpropertyChange.png)
462
463在本示例中,`Article`类被`@Observed`装饰器修饰。父组件`ArticleListView`传入`Article`对象实例给子组件`ArticleCard`,子组件使用`@ObjectLink`装饰器接收该实例。
464
4651. 当点击第1个文章卡片上的点赞图标时,会触发`ArticleCard`组件的`handleLiked`函数。该函数修改第1个卡片对应组件里`article`实例的`isLiked`和`likesCount`属性值。
4662. `article`实例是`@ObjectLink`装饰的状态变量,其属性值变化,会触发`ArticleCard`组件渲染,此时读取的`isLiked`和`likesCount`为修改后的新值。
467
468### 拖拽排序
469在List组件下使用ForEach,并设置[onMove](../../reference/apis-arkui/arkui-ts/ts-universal-attributes-drag-sorting.md#onmove)事件,每次迭代生成一个ListItem时,可以使能拖拽排序。拖拽排序离手后,如果数据位置发生变化,将触发onMove事件,上报数据移动原始索引号和目标索引号。在onMove事件中,需要根据上报的起始索引号和目标索引号修改数据源。数据源修改前后,要保持每个数据的键值不变,只是顺序发生变化,才能保证落位动画正常执行。
470
471```ts
472@Entry
473@Component
474struct ForEachSort {
475  @State arr: Array<string> = [];
476
477  build() {
478    Column() {
479      // 点击此按钮会触发ForEach重新渲染
480      Button('Add one item')
481        .onClick(() => {
482          this.arr.push('10');
483        })
484        .width(300)
485        .margin(10)
486
487      List() {
488        ForEach(this.arr, (item: string) => {
489          ListItem() {
490            Text(item.toString())
491              .fontSize(16)
492              .textAlign(TextAlign.Center)
493              .size({ height: 100, width: '100%' })
494          }.margin(10)
495          .borderRadius(10)
496          .backgroundColor('#FFFFFFFF')
497        }, (item: string) => item)
498          .onMove((from: number, to: number) => {
499            // 以下两行代码是为了确保拖拽后屏幕上组件的顺序与数组arr中每一项的顺序保持一致。
500            // 若注释以下两行,第一步拖拽排序,第二步在arr末尾插入一项,触发ForEach渲染,此时屏上组件的顺序会跟数组arr中每一项的顺序一致,而不是维持第一步拖拽后的顺序,意味着拖拽排序在ForEach渲染后失效了。
501            let tmp = this.arr.splice(from, 1);
502            this.arr.splice(to, 0, tmp[0]);
503          })
504      }
505      .width('100%')
506      .height('100%')
507      .backgroundColor('#FFDCDCDC')
508    }
509  }
510
511  aboutToAppear(): void {
512    for (let i = 0; i < 10; i++) {
513      this.arr.push(i.toString());
514    }
515  }
516}
517```
518
519**图8** ForEach拖拽排序效果图
520![ForEach-Drag-Sort](figures/ForEach-Drag-Sort.gif)
521
522注释掉`onMove`事件调用中的两行代码,点击`Add one item`触发渲染后的效果如下图所示。
523
524**图9** ForEach拖拽排序效果在重新渲染后没有保留
525![ForEach-Drag-Sort](figures/ForEach-Drag-Sort2.PNG)
526
527## 使用建议
528
529- 为满足键值的唯一性,对于对象数据类型,建议使用对象数据中的唯一`id`作为键值。
530- 不建议在键值中包含数据项索引`index`,可能会导致[渲染结果非预期](#渲染结果非预期)和[渲染性能降低](#渲染性能降低)。如果确实需要使用`index`,例如列表通过`index`进行条件渲染,开发者需接受`ForEach`在数据源变更后重新创建组件导致的性能损耗。
531- 基本类型数组的数据项没有唯一`ID`属性。如果使用数据项作为键值,必须确保数据项无重复。对于数据源会变化的场景,建议将基本类型数组转换为具有唯一`ID`属性的Object类型数组,再使用唯一`ID`属性作为键值。
532- 对于以上限制规则,`index`参数存在的意义为:index是开发者保证键值唯一性的最终手段;对数据项进行修改时,由于`itemGenerator`中的`item`参数是不可修改的,所以须用index索引值对数据源进行修改,进而触发UI重新渲染。
533- ForEach在下列容器组件 [List](../../reference/apis-arkui/arkui-ts/ts-container-list.md)、[Grid](../../reference/apis-arkui/arkui-ts/ts-container-grid.md)、[Swiper](../../reference/apis-arkui/arkui-ts/ts-container-swiper.md)以及[WaterFlow](../../reference/apis-arkui/arkui-ts/ts-container-waterflow.md) 内使用的时候,不要与[LazyForEach](./arkts-rendering-control-lazyforeach.md) 混用。 以List为例,同时包含ForEach、LazyForEach的情形是不推荐的。
534- 在大量子组件的的场景下,ForEach可能会导致卡顿。请考虑使用[LazyForEach](./arkts-rendering-control-lazyforeach.md)替代。最佳实践请参考[使用懒加载优化性能](https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-lazyforeach-optimization)535- 当数组项为对象类型时,不建议用内容相同的数组项替换旧项。若数组项发生变更但键值未变,会导致[数据变化不渲染](#数据变化不渲染)。
536## 不推荐案例
537
538对ForEach键值的错误使用会导致功能和性能问题,导致渲染效果非预期。详见案例[渲染结果非预期](#渲染结果非预期)和[渲染性能降低](#渲染性能降低)。
539
540### 渲染结果非预期
541
542在本示例中,通过设置`ForEach`的第三个参数`KeyGenerator`函数,自定义键值生成规则为数据源的索引`index`的字符串类型值。当点击父组件`Parent`中“在第1项后插入新项”文本组件后,界面会出现非预期的结果。
543
544```ts
545@Entry
546@Component
547struct Parent {
548  @State simpleList: Array<string> = ['one', 'two', 'three'];
549
550  build() {
551    Column() {
552      Button() {
553        Text('在第1项后插入新项').fontSize(30)
554      }
555      .onClick(() => {
556        this.simpleList.splice(1, 0, 'new item');
557      })
558
559      ForEach(this.simpleList, (item: string) => {
560        ChildItem({ item: item })
561      }, (item: string, index: number) => index.toString())
562    }
563    .justifyContent(FlexAlign.Center)
564    .width('100%')
565    .height('100%')
566    .backgroundColor(0xF1F3F5)
567  }
568}
569
570@Component
571struct ChildItem {
572  @Prop item: string;
573
574  build() {
575    Text(this.item)
576      .fontSize(30)
577  }
578}
579```
580
581上述代码的初始渲染效果和点击“在第1项后插入新项”文本组件后的渲染效果如下图所示。
582
583**图10**  渲染结果非预期运行效果图
584![ForEach-UnexpectedRenderingResult](figures/ForEach-UnexpectedRenderingResult.gif)
585
586`ForEach`在首次渲染时,创建的键值依次为"0"、"1"、"2"。
587
588插入新项后,数据源`simpleList`变为`['one', 'new item', 'two', 'three']`,框架监听到`@State`装饰的数据源长度变化触发`ForEach`重新渲染。
589
590`ForEach`依次遍历新数据源,遍历数据项"one"时生成键值"0",存在相同键值,因此不创建新组件。继续遍历数据项"new item"时生成键值"1",存在相同键值,因此不创建新组件。继续遍历数据项"two"生成键值"2",存在相同键值,因此不创建新组件。最后遍历数据项"three"时生成键值"3",不存在相同键值,创建内容为"three"的新组件并渲染。
591
592从以上可以看出,当键值包含数据项索引`index`时,期望的界面渲染结果为`['one', 'new item', 'two', 'three']`,而实际的渲染结果为`['one', 'two', 'three', 'three']`,不符合开发者预期。因此,开发者在使用`ForEach`时应避免键值包含索引`index`。
593
594### 渲染性能降低
595
596在本示例中,`ForEach`的第三个参数`KeyGenerator`函数缺省。根据上述[键值生成规则](#键值生成规则),此例使用框架默认的键值,即最终键值为字符串`index + '__' + JSON.stringify(item)`。点击文本组件“在第1项后插入新项”后,`ForEach`将为第2个数组项及后面的所有数据项重新创建组件。
597
598```ts
599@Entry
600@Component
601struct Parent {
602  @State simpleList: Array<string> = ['one', 'two', 'three'];
603
604  build() {
605    Column() {
606      Button() {
607        Text('在第1项后插入新项').fontSize(30)
608      }
609      .onClick(() => {
610        this.simpleList.splice(1, 0, 'new item');
611        console.info(`[onClick]: simpleList is [${this.simpleList.join(', ')}]`);
612      })
613
614      ForEach(this.simpleList, (item: string) => {
615        ChildItem({ item: item })
616      })
617    }
618    .justifyContent(FlexAlign.Center)
619    .width('100%')
620    .height('100%')
621    .backgroundColor(0xF1F3F5)
622  }
623}
624
625@Component
626struct ChildItem {
627  @Prop item: string;
628
629  aboutToAppear() {
630    console.info(`[aboutToAppear]: item is ${this.item}`);
631  }
632
633  build() {
634    Text(this.item)
635      .fontSize(50)
636  }
637}
638```
639
640以上代码的初始渲染效果和点击"在第1项后插入新项"文本组件后的渲染效果如下图所示。
641
642**图11**  渲染性能降低案例运行效果图
643![ForEach-RenderPerformanceDecrease](figures/ForEach-RenderPerformanceDecrease.gif)
644
645点击“在第1项后插入新项”文本组件后,DevEco Studio的日志打印结果如下所示。
646
647**图12**  渲染性能降低案例日志打印图
648![ForEach-RenderPerformanceDecreaseLogs](figures/ForEach-RenderPerformanceDecreaseLogs.png)
649
650插入新项后,`ForEach`为`new item`、 `two`、 `three`三个数组项创建了对应的`ChildItem`组件,并执行了组件的[`aboutToAppear()`](../../reference/apis-arkui/arkui-ts/ts-custom-component-lifecycle.md#abouttoappear)生命周期函数。这是因为:
651
6521. `ForEach`首次渲染时,生成的键值依次为`0__one`、`1__two`和`2__three`。
6532. 插入新项后,数据源`simpleList`变为`['one', 'new item', 'two', 'three']`,ArkUI框架监听到`@State`装饰的数据源长度变化触发`ForEach`重新渲染。
6543. `ForEach`依次遍历新数据源,遍历数据项`one`时生成键值`0__one`,键值已存在,因此不创建新组件。继续遍历数据项`new item`时生成键值`1__new item`,不存在相同键值,创建内容为`new item`的新组件并渲染。继续遍历数据项`two`生成键值`2__two`,不存在相同键值,创建内容为`two`的新组件并渲染。最后遍历数据项`three`时生成键值`3__three`,不存在相同键值,创建内容为`three`的新组件并渲染。
655
656尽管本例中界面渲染结果符合预期,但在每次向数组中间插入新数组项时,`ForEach`会为该数组项及其后面的所有数组项重新创建组件。当数据源数据量较大或组件结构复杂时,组件无法复用会导致性能下降。因此,不建议省略第三个参数`KeyGenerator`函数,也不建议在键值中使用数据项索引`index`。
657
658正确渲染并保证效率的`ForEach`写法是:
659```ts
660ForEach(this.simpleList, (item: string) => {
661  ChildItem({ item: item })
662}, (item: string) => item)  // 需要保证key唯一
663```
664提供了第三个参数`KeyGenerator`,在这个例子中,对数据源的不同数据项生成不同的key,并且对同一个数据项每次生成相同的key。
665
666### 数据变化不渲染
667点击按钮`Like/UnLike first article`,第一个组件会切换点赞手势和后面的点赞数量,但是点击按钮`Replace first article`之后再点击按钮`Like/UnLike first article`就不生效了。原因是替换`articleList[0]`之后,`articleList`状态变量发生变化,触发ForEach重新渲染,但是新的`articleList[0]`生成的key没有变,ForEach不会将数据更新同步给子组件,因此第一个组件仍然绑定旧的`articleList[0]`。新`articleList[0]`的属性发生变更,第一个组件感知不到,不会重新渲染。点击点赞手势,会触发渲染。因为变更的是跟组件绑定的数组项的属性,组件会感知并重新渲染。
668
669```ts
670@Observed
671class Article {
672  id: string;
673  title: string;
674  brief: string;
675  isLiked: boolean;
676  likesCount: number;
677
678  constructor(id: string, title: string, brief: string, isLiked: boolean, likesCount: number) {
679    this.id = id;
680    this.title = title;
681    this.brief = brief;
682    this.isLiked = isLiked;
683    this.likesCount = likesCount;
684  }
685}
686
687@Entry
688@Component
689struct ArticleListView {
690  @State articleList: Array<Article> = [
691    new Article('001', '第0篇文章', '文章简介内容', false, 100),
692    new Article('002', '第1篇文章', '文章简介内容', false, 100),
693    new Article('003', '第2篇文章', '文章简介内容', false, 100),
694    new Article('004', '第4篇文章', '文章简介内容', false, 100),
695    new Article('005', '第5篇文章', '文章简介内容', false, 100),
696    new Article('006', '第6篇文章', '文章简介内容', false, 100),
697  ];
698
699  build() {
700    Column() {
701      Button('Replace first article')
702        .onClick(() => {
703          this.articleList[0] = new Article('001', '第0篇文章', '文章简介内容', false, 100);
704        })
705        .width(300)
706        .margin(10)
707
708      Button('Like/Unlike first article')
709        .onClick(() => {
710          this.articleList[0].isLiked = !this.articleList[0].isLiked;
711          this.articleList[0].likesCount =
712            this.articleList[0].isLiked ? this.articleList[0].likesCount + 1 : this.articleList[0].likesCount - 1;
713        })
714        .width(300)
715        .margin(10)
716
717      List() {
718        ForEach(this.articleList, (item: Article) => {
719          ListItem() {
720            ArticleCard({
721              article: item
722            })
723              .margin({ top: 20 })
724          }
725        }, (item: Article) => item.id)
726      }
727      .padding(20)
728      .scrollBar(BarState.Off)
729      .backgroundColor(0xF1F3F5)
730    }
731  }
732}
733
734@Component
735struct ArticleCard {
736  @ObjectLink article: Article;
737
738  handleLiked() {
739    this.article.isLiked = !this.article.isLiked;
740    this.article.likesCount = this.article.isLiked ? this.article.likesCount + 1 : this.article.likesCount - 1;
741  }
742
743  build() {
744    Row() {
745      // 此处'app.media.icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
746      Image($r('app.media.icon'))
747        .width(80)
748        .height(80)
749        .margin({ right: 20 })
750
751      Column() {
752        Text(this.article.title)
753          .fontSize(20)
754          .margin({ bottom: 8 })
755        Text(this.article.brief)
756          .fontSize(16)
757          .fontColor(Color.Gray)
758          .margin({ bottom: 8 })
759
760        Row() {
761          // 此处app.media.iconLiked','app.media.iconUnLiked'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
762          Image(this.article.isLiked ? $r('app.media.iconLiked') : $r('app.media.iconUnLiked'))
763            .width(24)
764            .height(24)
765            .margin({ right: 8 })
766          Text(this.article.likesCount.toString())
767            .fontSize(16)
768        }
769        .onClick(() => this.handleLiked())
770        .justifyContent(FlexAlign.Center)
771      }
772      .alignItems(HorizontalAlign.Start)
773      .width('80%')
774      .height('100%')
775    }
776    .padding(20)
777    .borderRadius(12)
778    .backgroundColor('#FFECECEC')
779    .height(120)
780    .width('100%')
781    .justifyContent(FlexAlign.SpaceBetween)
782  }
783}
784```
785**图13** 数据变化不渲染
786![ForEach-StateVarNoRender](figures/ForEach-StateVarNoRender.PNG)
787
788### 非必要内存消耗
789如果开发者没有定义`keyGenerator`函数,则ArkUI框架会使用默认的键值生成函数,即`(item: Object, index: number) => { return index + '__' + JSON.stringify(item); }`。当`item`是复杂对象时,将其JSON序列化会得到长字符串,占用更多的内存。
790
791```ts
792class Data {
793  longStr: string;
794  key: string;
795
796  constructor(longStr: string, key: string) {
797    this.longStr = longStr;
798    this.key = key;
799  }
800}
801
802@Entry
803@Component
804struct Parent {
805  @State simpleList: Array<Data> = [];
806
807  aboutToAppear(): void {
808    let longStr = '';
809    for (let i = 0; i < 2000; i++) {
810      longStr += i.toString();
811    }
812    for (let index = 0; index < 3000; index++) {
813      let data: Data = new Data(longStr, 'a' + index.toString());
814      this.simpleList.push(data);
815    }
816  }
817
818  build() {
819    List() {
820      ForEach(this.simpleList, (item: Data) => {
821        ListItem() {
822          Text(item.key)
823        }
824      }
825        // 如果不定义下面的keyGenerator函数,则ArkUI框架会使用默认的键值生成函数
826        , (item: Data) => {
827          return item.key;
828        }
829      )
830    }.height('100%')
831    .width('100%')
832  }
833}
834```
835
836对比自定义`keyGenerator`函数和使用默认键值生成函数两种情况下的内存占用。自定义`keyGenerator`函数,这个示例代码的内存占用降低了约70MB。
837
838**图14** 使用默认键值生成函数下的内存占用
839![ForEach-StateVarNoRender](figures/ForEach-default-keyGenerator.PNG)
840
841**图15** 自定义键值生成函数下的内存占用
842![ForEach-StateVarNoRender](figures/ForEach-defined-keyGenerator.PNG)
843
844### 键值生成失败
845如果开发者没有定义`keyGenerator`函数,则ArkUI框架会使用默认的键值生成函数,即`(item: Object, index: number) => { return index + '__' + JSON.stringify(item); }`。然而,`JSON.stringify`序列化在某些数据结构上会失败,导致应用发生jscrash并退出。例如,`bigint`无法被`JSON.stringify`序列化:
846
847```ts
848class Data {
849  content: bigint;
850
851  constructor(content: bigint) {
852    this.content = content;
853  }
854}
855
856@Entry
857@Component
858struct Parent {
859  @State simpleList: Array<Data> = [new Data(1234567890123456789n), new Data(2345678910987654321n)];
860
861  build() {
862    Row() {
863      Column() {
864        ForEach(this.simpleList, (item: Data) => {
865          ChildItem({ item: item.content.toString() })
866        }
867          // 如果不定义下面的keyGenerator函数,则ArkUI框架会使用默认的键值生成函数
868          // Data中的content: bigint在JSON序列化时失败
869          , (item: Data) => item.content.toString()
870        )
871      }
872      .width('100%')
873      .height('100%')
874    }
875    .height('100%')
876    .backgroundColor(0xF1F3F5)
877  }
878}
879
880@Component
881struct ChildItem {
882  @Prop item: string;
883
884  build() {
885    Text(this.item)
886      .fontSize(50)
887  }
888}
889```
890
891开发者定义`keyGenerator`函数,应用正常启动:
892![ForEach-StateVarNoRender](figures/ForEach-defined-keyGenerator2.PNG)
893
894使用默认的键值生成函数,应用发生jscrash:
895```
896Error message:@Component 'Parent'[4]: ForEach id 7: use of default id generator function not possible on provided data structure. Need to specify id generator function (ForEach 3rd parameter). Application Error!
897Stacktrace:
898    ...
899    at anonymous (entry/src/main/ets/pages/Index.ets:18:52)
900```