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