1# ForEach:循环渲染
2
3ForEach接口基于数组类型数据来进行循环渲染,需要与容器组件配合使用,且接口返回的组件应当是允许包含在ForEach父容器组件中的子组件。例如,ListItem组件要求ForEach的父容器组件必须为[List组件](../reference/apis-arkui/arkui-ts/ts-container-list.md)。
4
5API参数说明见:[ForEach API参数说明](../reference/apis-arkui/arkui-ts/ts-rendering-control-foreach.md)
6
7> **说明:**
8>
9> 从API version 9开始,该接口支持在ArkTS卡片中使用。
10
11## 键值生成规则
12
13在`ForEach`循环渲染过程中,系统会为每个数组元素生成一个唯一且持久的键值,用于标识对应的组件。当这个键值变化时,ArkUI框架将视为该数组元素已被替换或修改,并会基于新的键值创建一个新的组件。
14
15`ForEach`提供了一个名为`keyGenerator`的参数,这是一个函数,开发者可以通过它自定义键值的生成规则。如果开发者没有定义`keyGenerator`函数,则ArkUI框架会使用默认的键值生成函数,即`(item: Object, index: number) => { return index + '__' + JSON.stringify(item); }`。
16
17ArkUI框架对于`ForEach`的键值生成有一套特定的判断规则,这主要与`itemGenerator`函数的第二个参数`index`以及`keyGenerator`函数的第二个参数`index`有关,具体的键值生成规则判断逻辑如下图所示。
18
19**图1** ForEach键值生成规则
20![ForEach-Key-Generation-Rules](figures/ForEach-Key-Generation-Rules.png)
21
22> **说明:**
23>
24> ArkUI框架会对重复的键值发出警告。在UI更新的场景下,如果出现重复的键值,框架可能无法正常工作,具体请参见[渲染结果非预期](#渲染结果非预期)。
25
26## 组件创建规则
27
28在确定键值生成规则后,ForEach的第二个参数`itemGenerator`函数会根据键值生成规则为数据源的每个数组项创建组件。组件的创建包括两种情况:[ForEach首次渲染](#首次渲染)和[ForEach非首次渲染](#非首次渲染)。
29
30### 首次渲染
31
32在ForEach首次渲染时,会根据前述键值生成规则为数据源的每个数组项生成唯一键值,并创建相应的组件。
33
34```ts
35@Entry
36@Component
37struct Parent {
38  @State simpleList: Array<string> = ['one', 'two', 'three'];
39
40  build() {
41    Row() {
42      Column() {
43        ForEach(this.simpleList, (item: string) => {
44          ChildItem({ item: item })
45        }, (item: string) => item)
46      }
47      .width('100%')
48      .height('100%')
49    }
50    .height('100%')
51    .backgroundColor(0xF1F3F5)
52  }
53}
54
55@Component
56struct ChildItem {
57  @Prop item: string;
58
59  build() {
60    Text(this.item)
61      .fontSize(50)
62  }
63}
64```
65
66运行效果如下图所示。
67
68**图2**  ForEach数据源不存在相同值案例首次渲染运行效果图
69![ForEach-CaseStudy-1stRender-NoDup](figures/ForEach-CaseStudy-1stRender-NoDup.png)
70
71在上述代码中,键值生成规则是`keyGenerator`函数的返回值`item`。在ForEach渲染循环时,为数据源数组项依次生成键值`one`、`two`和`three`,并创建对应的`ChildItem`组件渲染到界面上。
72
73当不同数组项按照键值生成规则生成的键值相同时,框架的行为是未定义的。例如,在以下代码中,ForEach渲染相同的数据项`two`时,只创建了一个`ChildItem`组件,而没有创建多个具有相同键值的组件。
74
75 ```ts
76 @Entry
77 @Component
78 struct Parent {
79   @State simpleList: Array<string> = ['one', 'two', 'two', 'three'];
80
81   build() {
82     Row() {
83       Column() {
84         ForEach(this.simpleList, (item: string) => {
85           ChildItem({ item: item })
86         }, (item: string) => item)
87       }
88       .width('100%')
89       .height('100%')
90     }
91     .height('100%')
92     .backgroundColor(0xF1F3F5)
93   }
94 }
95
96 @Component
97 struct ChildItem {
98   @Prop item: string;
99
100   build() {
101     Text(this.item)
102       .fontSize(50)
103   }
104 }
105 ```
106
107运行效果如下图所示。
108
109**图3**  ForEach数据源存在相同值案例首次渲染运行效果图
110![ForEach-CaseStudy-1stRender-Dup](figures/ForEach-CaseStudy-1stRender-Dup.png)
111
112在该示例中,最终键值生成规则为`item`。当ForEach遍历数据源`simpleList`,遍历到索引为1的`two`时,按照最终键值生成规则生成键值为`two`的组件并进行标记。当遍历到索引为2的`two`时,按照最终键值生成规则当前项的键值也为`two`,此时不再创建新的组件。
113
114### 非首次渲染
115
116在ForEach组件进行非首次渲染时,它会检查新生成的键值是否在上次渲染中已经存在。如果键值不存在,则会创建一个新的组件;如果键值存在,则不会创建新的组件,而是直接渲染该键值所对应的组件。例如,在以下的代码示例中,通过点击事件修改了数组的第三项值为"new three",这将触发ForEach组件进行非首次渲染。
117
118```ts
119@Entry
120@Component
121struct Parent {
122  @State simpleList: Array<string> = ['one', 'two', 'three'];
123
124  build() {
125    Row() {
126      Column() {
127        Text('点击修改第3个数组项的值')
128          .fontSize(24)
129          .fontColor(Color.Red)
130          .onClick(() => {
131            this.simpleList[2] = 'new three';
132          })
133
134        ForEach(this.simpleList, (item: string) => {
135          ChildItem({ item: item })
136            .margin({ top: 20 })
137        }, (item: string) => item)
138      }
139      .justifyContent(FlexAlign.Center)
140      .width('100%')
141      .height('100%')
142    }
143    .height('100%')
144    .backgroundColor(0xF1F3F5)
145  }
146}
147
148@Component
149struct ChildItem {
150  @Prop item: string;
151
152  build() {
153    Text(this.item)
154      .fontSize(30)
155  }
156}
157```
158
159运行效果如下图所示。
160
161**图4**  ForEach非首次渲染案例运行效果图
162![ForEach-Non-Initial-Render-Case-Effect](figures/ForEach-Non-Initial-Render-Case-Effect.gif)
163
164从本例可以看出`@State` 能够监听到简单数据类型数组数据源 `simpleList` 数组项的变化。
165
1661. 当 `simpleList` 数组项发生变化时,会触发 `ForEach` 进行重新渲染。
1672. `ForEach` 遍历新的数据源 `['one', 'two', 'new three']`,并生成对应的键值`one`、`two`和`new three`。
1683. 其中,键值`one`和`two`在上次渲染中已经存在,所以 `ForEach` 复用了对应的组件并进行了渲染。对于第三个数组项 "new three",由于其通过键值生成规则 `item` 生成的键值`new three`在上次渲染中不存在,因此 `ForEach` 为该数组项创建了一个新的组件。
169
170## 使用场景
171
172ForEach组件在开发过程中的主要应用场景包括:[数据源不变](#数据源不变)、[数据源数组项发生变化](#数据源数组项发生变化)(如插入、删除操作)、[数据源数组项子属性变化](#数据源数组项子属性变化)。
173
174### 数据源不变
175
176在数据源保持不变的场景中,数据源可以直接采用基本数据类型。例如,在页面加载状态时,可以使用骨架屏列表进行渲染展示。
177
178```ts
179@Entry
180@Component
181struct ArticleList {
182  @State simpleList: Array<number> = [1, 2, 3, 4, 5];
183
184  build() {
185    Column() {
186      ForEach(this.simpleList, (item: number) => {
187        ArticleSkeletonView()
188          .margin({ top: 20 })
189      }, (item: number) => item.toString())
190    }
191    .padding(20)
192    .width('100%')
193    .height('100%')
194  }
195}
196
197@Builder
198function textArea(width: number | Resource | string = '100%', height: number | Resource | string = '100%') {
199  Row()
200    .width(width)
201    .height(height)
202    .backgroundColor('#FFF2F3F4')
203}
204
205@Component
206struct ArticleSkeletonView {
207  build() {
208    Row() {
209      Column() {
210        textArea(80, 80)
211      }
212      .margin({ right: 20 })
213
214      Column() {
215        textArea('60%', 20)
216        textArea('50%', 20)
217      }
218      .alignItems(HorizontalAlign.Start)
219      .justifyContent(FlexAlign.SpaceAround)
220      .height('100%')
221    }
222    .padding(20)
223    .borderRadius(12)
224    .backgroundColor('#FFECECEC')
225    .height(120)
226    .width('100%')
227    .justifyContent(FlexAlign.SpaceBetween)
228  }
229}
230```
231
232运行效果如下图所示。
233
234**图5** 骨架屏运行效果图
235![ForEach-SkeletonScreen](figures/ForEach-SkeletonScreen.png)
236
237在本示例中,采用数据项item作为键值生成规则,由于数据源simpleList的数组项各不相同,因此能够保证键值的唯一性。
238
239### 数据源数组项发生变化
240
241在数据源数组项发生变化的场景下,例如进行数组插入、删除操作或者数组项索引位置发生交换时,数据源应为对象数组类型,并使用对象的唯一ID作为最终键值。例如,当在页面上通过手势上滑加载下一页数据时,会在数据源数组尾部新增新获取的数据项,从而使得数据源数组长度增大。
242
243```ts
244class Article {
245  id: string;
246  title: string;
247  brief: string;
248
249  constructor(id: string, title: string, brief: string) {
250    this.id = id;
251    this.title = title;
252    this.brief = brief;
253  }
254}
255
256@Entry
257@Component
258struct ArticleListView {
259  @State isListReachEnd: boolean = false;
260  @State articleList: Array<Article> = [
261    new Article('001', '第1篇文章', '文章简介内容'),
262    new Article('002', '第2篇文章', '文章简介内容'),
263    new Article('003', '第3篇文章', '文章简介内容'),
264    new Article('004', '第4篇文章', '文章简介内容'),
265    new Article('005', '第5篇文章', '文章简介内容'),
266    new Article('006', '第6篇文章', '文章简介内容')
267  ]
268
269  loadMoreArticles() {
270    this.articleList.push(new Article('007', '加载的新文章', '文章简介内容'));
271  }
272
273  build() {
274    Column({ space: 5 }) {
275      List() {
276        ForEach(this.articleList, (item: Article) => {
277          ListItem() {
278            ArticleCard({ article: item })
279              .margin({ top: 20 })
280          }
281        }, (item: Article) => item.id)
282      }
283      .onReachEnd(() => {
284        this.isListReachEnd = true;
285      })
286      .parallelGesture(
287        PanGesture({ direction: PanDirection.Up, distance: 80 })
288          .onActionStart(() => {
289            if (this.isListReachEnd) {
290              this.loadMoreArticles();
291              this.isListReachEnd = false;
292            }
293          })
294      )
295      .padding(20)
296      .scrollBar(BarState.Off)
297    }
298    .width('100%')
299    .height('100%')
300    .backgroundColor(0xF1F3F5)
301  }
302}
303
304@Component
305struct ArticleCard {
306  @Prop article: Article;
307
308  build() {
309    Row() {
310      // 此处'app.media.icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
311      Image($r('app.media.icon'))
312        .width(80)
313        .height(80)
314        .margin({ right: 20 })
315
316      Column() {
317        Text(this.article.title)
318          .fontSize(20)
319          .margin({ bottom: 8 })
320        Text(this.article.brief)
321          .fontSize(16)
322          .fontColor(Color.Gray)
323          .margin({ bottom: 8 })
324      }
325      .alignItems(HorizontalAlign.Start)
326      .width('80%')
327      .height('100%')
328    }
329    .padding(20)
330    .borderRadius(12)
331    .backgroundColor('#FFECECEC')
332    .height(120)
333    .width('100%')
334    .justifyContent(FlexAlign.SpaceBetween)
335  }
336}
337```
338
339初始运行效果(左图)和手势上滑加载后效果(右图)如下图所示。
340
341**图6**  数据源数组项变化案例运行效果图
342![ForEach-DataSourceArrayChange](figures/ForEach-DataSourceArrayChange.png)
343
344在本示例中,`ArticleCard`组件作为`ArticleListView`组件的子组件,通过`@Prop`装饰器接收一个`Article`对象,用于渲染文章卡片。
345
3461. 当列表滚动到底部时,如果手势滑动距离超过指定的80,将触发`loadMoreArticle()`函数。此函数会在`articleList`数据源的尾部添加一个新的数据项,从而增加数据源的长度。
3472. 数据源被`@State`装饰器修饰,ArkUI框架能够感知到数据源长度的变化,并触发`ForEach`进行重新渲染。
348
349### 数据源数组项子属性变化
350
351当数据源的数组项为对象数据类型,并且只修改某个数组项的属性值时,由于数据源为复杂数据类型,ArkUI框架无法监听到`@State`装饰器修饰的数据源数组项的属性变化,从而无法触发`ForEach`的重新渲染。为实现`ForEach`重新渲染,需要结合`@Observed`和`@ObjectLink`装饰器使用。例如,在文章列表卡片上点击“点赞”按钮,从而修改文章的点赞数量。
352
353```ts
354@Observed
355class Article {
356  id: string;
357  title: string;
358  brief: string;
359  isLiked: boolean;
360  likesCount: number;
361
362  constructor(id: string, title: string, brief: string, isLiked: boolean, likesCount: number) {
363    this.id = id;
364    this.title = title;
365    this.brief = brief;
366    this.isLiked = isLiked;
367    this.likesCount = likesCount;
368  }
369}
370
371@Entry
372@Component
373struct ArticleListView {
374  @State articleList: Array<Article> = [
375    new Article('001', '第0篇文章', '文章简介内容', false, 100),
376    new Article('002', '第1篇文章', '文章简介内容', false, 100),
377    new Article('003', '第2篇文章', '文章简介内容', false, 100),
378    new Article('004', '第4篇文章', '文章简介内容', false, 100),
379    new Article('005', '第5篇文章', '文章简介内容', false, 100),
380    new Article('006', '第6篇文章', '文章简介内容', false, 100),
381  ];
382
383  build() {
384    List() {
385      ForEach(this.articleList, (item: Article) => {
386        ListItem() {
387          ArticleCard({
388            article: item
389          })
390            .margin({ top: 20 })
391        }
392      }, (item: Article) => item.id)
393    }
394    .padding(20)
395    .scrollBar(BarState.Off)
396    .backgroundColor(0xF1F3F5)
397  }
398}
399
400@Component
401struct ArticleCard {
402  @ObjectLink article: Article;
403
404  handleLiked() {
405    this.article.isLiked = !this.article.isLiked;
406    this.article.likesCount = this.article.isLiked ? this.article.likesCount + 1 : this.article.likesCount - 1;
407  }
408
409  build() {
410    Row() {
411      // 此处'app.media.icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
412      Image($r('app.media.icon'))
413        .width(80)
414        .height(80)
415        .margin({ right: 20 })
416
417      Column() {
418        Text(this.article.title)
419          .fontSize(20)
420          .margin({ bottom: 8 })
421        Text(this.article.brief)
422          .fontSize(16)
423          .fontColor(Color.Gray)
424          .margin({ bottom: 8 })
425
426        Row() {
427          // 此处app.media.iconLiked','app.media.iconUnLiked'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
428          Image(this.article.isLiked ? $r('app.media.iconLiked') : $r('app.media.iconUnLiked'))
429            .width(24)
430            .height(24)
431            .margin({ right: 8 })
432          Text(this.article.likesCount.toString())
433            .fontSize(16)
434        }
435        .onClick(() => this.handleLiked())
436        .justifyContent(FlexAlign.Center)
437      }
438      .alignItems(HorizontalAlign.Start)
439      .width('80%')
440      .height('100%')
441    }
442    .padding(20)
443    .borderRadius(12)
444    .backgroundColor('#FFECECEC')
445    .height(120)
446    .width('100%')
447    .justifyContent(FlexAlign.SpaceBetween)
448  }
449}
450```
451
452上述代码的初始运行效果(左图)和点击第1个文章卡片上的点赞图标后的运行效果(右图)如下图所示。
453
454**图7** 数据源数组项子属性变化案例运行效果图
455![ForEach-DataSourceArraySubpropertyChange](figures/ForEach-DataSourceArraySubpropertyChange.png)
456
457在本示例中,`Article`类被`@Observed`装饰器修饰。父组件`ArticleListView`传入`Article`对象实例给子组件`ArticleCard`,子组件使用`@ObjectLink`装饰器接收该实例。
458
4591. 当点击第1个文章卡片上的点赞图标时,会触发`ArticleCard`组件的`handleLiked`函数。该函数修改第1个卡片对应组件里`article`实例的`isLiked`和`likesCount`属性值。
4602. 由于子组件`ArticleCard`中的`article`使用了`@ObjectLink`装饰器,父子组件共享同一份`article`数据。因此,父组件中`articleList`的第1个数组项的`isLiked`和`likedCounts`数值也会同步修改。
4613. 当父组件监听到数据源数组项属性值变化时,会触发`ForEach`重新渲染。
4624. 在此处,`ForEach`键值生成规则为数组项的`id`属性值。当`ForEach`遍历新数据源时,数组项的`id`均没有变化,不会新建组件。
4635. 渲染第1个数组项对应的`ArticleCard`组件时,读取到的`isLiked`和`likesCount`为修改后的新值。
464
465### 拖拽排序
466当ForEach在List组件下使用,并且设置了onMove事件,ForEach每次迭代都生成一个ListItem时,可以使能拖拽排序。拖拽排序离手后,如果数据位置发生变化,则会触发onMove事件,上报数据移动原始索引号和目标索引号。在onMove事件中,需要根据上报的起始索引号和目标索引号修改数据源。数据源修改前后,要保持每个数据的键值不变,只是顺序发生变化,才能保证落位动画正常执行。
467
468```ts
469@Entry
470@Component
471struct ForEachSort {
472  @State arr: Array<string> = [];
473
474  build() {
475    Row() {
476      List() {
477        ForEach(this.arr, (item: string) => {
478          ListItem() {
479            Text(item.toString())
480              .fontSize(16)
481              .textAlign(TextAlign.Center)
482              .size({height: 100, width: "100%"})
483          }.margin(10)
484          .borderRadius(10)
485          .backgroundColor("#FFFFFFFF")
486        }, (item: string) => item)
487          .onMove((from:number, to:number) => {
488            let tmp = this.arr.splice(from, 1);
489            this.arr.splice(to, 0, tmp[0])
490          })
491      }
492      .width('100%')
493      .height('100%')
494      .backgroundColor("#FFDCDCDC")
495    }
496  }
497  aboutToAppear(): void {
498    for (let i = 0; i < 100; i++) {
499      this.arr.push(i.toString())
500    }
501  }
502}
503```
504
505**图8** ForEach拖拽排序效果图
506![ForEach-Drag-Sort](figures/ForEach-Drag-Sort.gif)
507## 使用建议
508
509- 为满足键值的唯一性,对于对象数据类型,建议使用对象数据中的唯一`id`作为键值。
510- 尽量避免在最终的键值生成规则中包含数据项索引`index`,以防止出现[渲染结果非预期](#渲染结果非预期)和[渲染性能降低](#渲染性能降低)。如果业务确实需要使用`index`,例如列表需要通过`index`进行条件渲染,开发者需要接受`ForEach`在改变数据源后重新创建组件所带来的性能损耗。
511- 基本数据类型的数据项没有唯一`ID`属性。如果使用基本数据类型本身作为键值,必须确保数组项无重复。因此,对于数据源会发生变化的场景,建议将基本数据类型数组转化为具备唯一`ID`属性的对象数据类型数组,再使用`ID`属性作为键值生成规则。
512- 对于以上限制规则,`index`参数存在的意义为:index是开发者保证键值唯一性的最终手段;对数据项进行修改时,由于`itemGenerator`中的`item`参数是不可修改的,所以须用index索引值对数据源进行修改,进而触发UI重新渲染。
513- 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的情形是不推荐的。
514
515## 不推荐案例
516
517开发者在使用ForEach的过程中,若对于键值生成规则的理解不够充分,可能会出现错误的使用方式。错误使用一方面会导致功能层面问题,例如[渲染结果非预期](#渲染结果非预期),另一方面会导致性能层面问题,例如[渲染性能降低](#渲染性能降低)。
518
519### 渲染结果非预期
520
521在本示例中,通过设置`ForEach`的第三个参数`KeyGenerator`函数,自定义键值生成规则为数据源的索引`index`的字符串类型值。当点击父组件`Parent`中“在第1项后插入新项”文本组件后,界面会出现非预期的结果。
522
523```ts
524@Entry
525@Component
526struct Parent {
527  @State simpleList: Array<string> = ['one', 'two', 'three'];
528
529  build() {
530    Column() {
531      Button() {
532        Text('在第1项后插入新项').fontSize(30)
533      }
534      .onClick(() => {
535        this.simpleList.splice(1, 0, 'new item');
536      })
537
538      ForEach(this.simpleList, (item: string) => {
539        ChildItem({ item: item })
540      }, (item: string, index: number) => index.toString())
541    }
542    .justifyContent(FlexAlign.Center)
543    .width('100%')
544    .height('100%')
545    .backgroundColor(0xF1F3F5)
546  }
547}
548
549@Component
550struct ChildItem {
551  @Prop item: string;
552
553  build() {
554    Text(this.item)
555      .fontSize(30)
556  }
557}
558```
559
560上述代码的初始渲染效果和点击“在第1项后插入新项”文本组件后的渲染效果如下图所示。
561
562**图9**  渲染结果非预期运行效果图
563![ForEach-UnexpectedRenderingResult](figures/ForEach-UnexpectedRenderingResult.gif)
564
565`ForEach`在首次渲染时,创建的键值依次为"0"、"1"、"2"。
566
567插入新项后,数据源`simpleList`变为`['one', 'new item', 'two', 'three']`,框架监听到`@State`装饰的数据源长度变化触发`ForEach`重新渲染。
568
569`ForEach`依次遍历新数据源,遍历数据项"one"时生成键值"0",存在相同键值,因此不创建新组件。继续遍历数据项"new item"时生成键值"1",存在相同键值,因此不创建新组件。继续遍历数据项"two"生成键值"2",存在相同键值,因此不创建新组件。最后遍历数据项"three"时生成键值"3",不存在相同键值,创建内容为"three"的新组件并渲染。
570
571从以上可以看出,当最终键值生成规则包含`index`时,期望的界面渲染结果为`['one', 'new item', 'two', 'three']`,而实际的渲染结果为`['one', 'two', 'three', 'three']`,渲染结果不符合开发者预期。因此,开发者在使用`ForEach`时应尽量避免最终键值生成规则中包含`index`。
572
573### 渲染性能降低
574
575在本示例中,`ForEach`的第三个参数`KeyGenerator`函数处于缺省状态。根据上述[键值生成规则](#键值生成规则),此例使用框架默认的键值生成规则,即最终键值为字符串`index + '__' + JSON.stringify(item)`。当点击“在第1项后插入新项”文本组件后,`ForEach`将需要为第2个数组项以及其后的所有项重新创建组件。
576
577```ts
578@Entry
579@Component
580struct Parent {
581  @State simpleList: Array<string> = ['one', 'two', 'three'];
582
583  build() {
584    Column() {
585      Button() {
586        Text('在第1项后插入新项').fontSize(30)
587      }
588      .onClick(() => {
589        this.simpleList.splice(1, 0, 'new item');
590        console.log(`[onClick]: simpleList is ${JSON.stringify(this.simpleList)}`);
591      })
592
593      ForEach(this.simpleList, (item: string) => {
594        ChildItem({ item: item })
595      })
596    }
597    .justifyContent(FlexAlign.Center)
598    .width('100%')
599    .height('100%')
600    .backgroundColor(0xF1F3F5)
601  }
602}
603
604@Component
605struct ChildItem {
606  @Prop item: string;
607
608  aboutToAppear() {
609    console.log(`[aboutToAppear]: item is ${this.item}`);
610  }
611
612  build() {
613    Text(this.item)
614      .fontSize(50)
615  }
616}
617```
618
619以上代码的初始渲染效果和点击"在第1项后插入新项"文本组件后的渲染效果如下图所示。
620
621**图10**  渲染性能降低案例运行效果图
622![ForEach-RenderPerformanceDecrease](figures/ForEach-RenderPerformanceDecrease.gif)
623
624点击“在第1项后插入新项”文本组件后,DevEco Studio的日志打印结果如下所示。
625
626**图11**  渲染性能降低案例日志打印图
627![ForEach-RenderPerformanceDecreaseLogs](figures/ForEach-RenderPerformanceDecreaseLogs.png)
628
629插入新项后,`ForEach`为`new item`、 `two`、 `three`三个数组项创建了对应的组件`ChildItem`,并执行了组件的[`aboutToAppear()`](../reference/apis-arkui/arkui-ts/ts-custom-component-lifecycle.md#abouttoappear)生命周期函数。这是因为:
630
6311. 在`ForEach`首次渲染时,创建的键值依次为`0__one`、`1__two`、`2__three`。
6322. 插入新项后,数据源`simpleList`变为`['one', 'new item', 'two', 'three']`,ArkUI框架监听到`@State`装饰的数据源长度变化触发`ForEach`重新渲染。
6333. `ForEach`依次遍历新数据源,遍历数据项`one`时生成键值`0__one`,键值已存在,因此不创建新组件。继续遍历数据项`new item`时生成键值`1__new item`,不存在相同键值,创建内容为`new item`的新组件并渲染。继续遍历数据项`two`生成键值`2__two`,不存在相同键值,创建内容为`two`的新组件并渲染。最后遍历数据项`three`时生成键值`3__three`,不存在相同键值,创建内容为`three`的新组件并渲染。
634
635尽管此示例中界面渲染的结果符合预期,但每次插入一条新数组项时,`ForEach`都会为从该数组项起后面的所有数组项全部重新创建组件。当数据源数据量较大或组件结构复杂时,由于组件无法得到复用,将导致性能体验不佳。因此,除非必要,否则不推荐将第三个参数`KeyGenerator`函数处于缺省状态,以及在键值生成规则中包含数据项索引`index`。
636正确渲染并保证效率的`ForEach`写法是:
637```ts
638ForEach(this.simpleList, (item: string) => {
639  ChildItem({ item: item })
640}, (item: string) => item)  // 需要保证key唯一
641```
642提供了第三个参数`KeyGenerator`,在这个例子中,对数据源的不同数据项生成不同的key,并且对同一个数据项每次生成相同的key。