1# ForEach:循环渲染 2 3 4ForEach基于数组类型数据执行循环渲染。 5 6> **说明:** 7> 8> 从API version 9开始,该接口支持在ArkTS卡片中使用。 9 10## 接口描述 11 12 13```ts 14ForEach( 15 arr: Array, 16 itemGenerator: (item: Array, index?: number) => void, 17 keyGenerator?: (item: Array, index?: number): string => string 18) 19``` 20 21 22| 参数名 | 参数类型 | 必填 | 参数描述 | 23| ------------- | ---------------------------------------- | ---- | ---------------------------------------- | 24| arr | Array | 是 | 必须是数组,允许设置为空数组,空数组场景下将不会创建子组件。同时允许设置返回值为数组类型的函数,例如arr.slice(1, 3),设置的函数不得改变包括数组本身在内的任何状态变量,如Array.splice、Array.sort或Array.reverse这些改变原数组的函数。 | 25| itemGenerator | (item: any, index?: number) => void | 是 | 生成子组件的lambda函数,为数组中的每一个数据项创建一个或多个子组件,单个子组件或子组件列表必须包括在大括号“{...}”中。<br/>**说明:**<br/>- 子组件的类型必须是ForEach的父容器组件所允许的(例如,只有当ForEach父级为List组件时,才允许ListItem子组件)。<br/>- 允许子类构造函数返回if或另一个ForEach。ForEach可以在if内的任意位置。<br/>- 可选index参数如在函数体中使用,则必须仅在函数签名中指定。 | 26| keyGenerator | (item: any, index?: number) => string | 否 | 匿名函数,用于给数组中的每一个数据项生成唯一且固定的键值。键值生成器的功能是可选的,但是,为了使开发框架能够更好地识别数组更改,提高性能,建议提供。如将数组反向时,如果没有提供键值生成器,则ForEach中的所有节点都将重建。<br/>**说明:**<br/>- 同一数组中的不同项绝对不能计算出相同的ID。<br/>- 如果未使用index参数,则项在数组中的位置变动不得改变项的键值。如果使用了index参数,则当项在数组中的位置有变动时,键值必须更改。<br/>- 当某个项目被新项替换(值不同)时,被替换的项键值和新项的键值必须不同。<br/>- 在构造函数中使用index参数时,键值生成函数也必须使用该参数。<br>- 键值生成函数不允许改变任何组件状态。 | 27 28 29## 使用限制 30 31- ForEach必须在容器组件内使用。 32 33- 生成的子组件应当是允许包含在ForEach父容器组件中的子组件。 34 35- 允许子组件生成器函数中包含if/else条件渲染,同时也允许ForEach包含在if/else条件渲染语句中。 36 37- itemGenerator函数的调用顺序不一定和数组中的数据项相同,在开发过程中不要假设itemGenerator和keyGenerator函数是否执行及其执行顺序。例如,以下示例可能无法正确运行: 38 39 ```ts 40 let obj: Object 41 ForEach(anArray.map((item1: Object, index1: number): Object => { 42 obj.i = index1 + 1 43 obj.data = item1 44 return obj; 45 }), 46 (item: string) => Text(`${item.i}. item.data.label`), 47 (item: string): string => { 48 return item.data.id.toString() 49 }) 50 ``` 51 52 53## 开发建议 54 55- 建议开发者不要假设项构造函数的执行顺序。执行顺序可能不能是数组中项的排列顺序。 56 57- 不要假设数组项是否是初始渲染。ForEach的初始渲染在\@Component首次渲染时构建所有数组项。后续框架版本中可能会将此行为更改为延迟加载模式。 58 59- 使用 index参数对UI更新性能有严重的负面影响,请尽量避免。 60 61- 如果项构造函数中使用index参数,则项索引函数中也必须使用该参数。否则,如果项索引函数未使用index参数,ForEach在生成实际的键值时,框架也会把index考虑进来,默认将index拼接在后面。 62 63 64## 使用场景 65 66 67### 简单ForEach示例 68 69根据arr数据分别创建3个Text和Divide组件。 70 71 72```ts 73@Entry 74@Component 75struct MyComponent { 76 @State arr: number[] = [10, 20, 30]; 77 78 build() { 79 Column({ space: 5 }) { 80 Button('Reverse Array') 81 .onClick(() => { 82 this.arr.reverse(); 83 }) 84 ForEach(this.arr, (item: number) => { 85 Text(`item value: ${item}`).fontSize(18) 86 Divider().strokeWidth(2) 87 }, (item: number) => item.toString()) 88 } 89 } 90} 91``` 92 93 94### 复杂ForEach示例 95 96 97```ts 98@Component 99struct CounterView { 100 @State label: string = ""; 101 @State count: number = 0; 102 103 build() { 104 Button(`${this.label}-${this.count} click +1`) 105 .width(300).height(40) 106 .backgroundColor('#a0ffa0') 107 .onClick(() => { 108 this.count++; 109 }) 110 } 111} 112 113@Entry 114@Component 115struct MainView { 116 @State arr: number[] = Array.from(Array(10).keys()); // [0.,.9] 117 nextUnused: number = this.arr.length; 118 119 build() { 120 Column() { 121 Button(`push new item`) 122 .onClick(() => { 123 this.arr.push(this.nextUnused++) 124 }) 125 .width(300).height(40) 126 Button(`pop last item`) 127 .onClick(() => { 128 this.arr.pop() 129 }) 130 .width(300).height(40) 131 Button(`prepend new item (unshift)`) 132 .onClick(() => { 133 this.arr.unshift(this.nextUnused++) 134 }) 135 .width(300).height(40) 136 Button(`remove first item (shift)`) 137 .onClick(() => { 138 this.arr.shift() 139 }) 140 .width(300).height(40) 141 Button(`insert at pos ${Math.floor(this.arr.length / 2)}`) 142 .onClick(() => { 143 this.arr.splice(Math.floor(this.arr.length / 2), 0, this.nextUnused++); 144 }) 145 .width(300).height(40) 146 Button(`remove at pos ${Math.floor(this.arr.length / 2)}`) 147 .onClick(() => { 148 this.arr.splice(Math.floor(this.arr.length / 2), 1); 149 }) 150 .width(300).height(40) 151 Button(`set at pos ${Math.floor(this.arr.length / 2)} to ${this.nextUnused}`) 152 .onClick(() => { 153 this.arr[Math.floor(this.arr.length / 2)] = this.nextUnused++; 154 }) 155 .width(300).height(40) 156 ForEach(this.arr, 157 (item: string) => { 158 CounterView({ label: item.toString() }) 159 }, 160 (item: string) => item.toString() 161 ) 162 } 163 } 164} 165``` 166 167MainView拥有一个\@State装饰的数字数组。添加、删除和替换数组项是可观察到的变化事件,当这些事件发生时,MainView内的ForEach都会更新。 168 169项目索引函数为每个数组项创建唯一且持久的键值,ArkUI框架通过此键值确定数组中的项是否有变化,只要键值相同,数组项的值就假定不变,但其索引位置可能会更改。此机制的运行前提是不同的数组项不能有相同的键值。 170 171使用计算出的ID,框架可以对添加、删除和保留的数组项加以区分: 172 1731. 框架将删除已删除数组项的UI组件。 174 1752. 框架仅对新添加的数组项执行项构造函数。 176 1773. 框架不会为保留的数组项执行项构造函数。如果数组中的项索引已更改,框架将仅根据新顺序移动其UI组件,但不会更新该UI组件。 178 179建议使用项目索引函数,但这是可选的。生成的ID必须是唯一的,这意味着不能为数组中的不同项计算出相同的ID。即使两个数组项具有相同的值,其ID也必须不同。 180 181如果数组项值更改,则ID必须更改。 182如前所述,id生成函数是可选的。以下是不带项索引函数的ForEach: 183 184 ```ts 185let list: Object 186ForEach(this.arr, 187 (item: Object): string => { 188 list.label = item.toString(); 189 CounterView(list) 190 } 191 ) 192 ``` 193 194如果没有提供项ID函数,则框架会尝试在更新ForEach时智能检测数组更改。但是,它可能会删除子组件,并为在数组中移动(索引被更改)的数组项重新执行项构造函数。在上面的示例中,这将更改应用程序针对CounterView counter状态的行为。创建新的CounterView实例时,counter的值将初始化为0。 195 196 197### 使用\@ObjectLink的ForEach示例 198 199当需要保留重复子组件的状态时,\@ObjectLink可将状态在组件树中向父组件推送。 200 201 202```ts 203let NextID: number = 0; 204 205@Observed 206class MyCounter { 207 public id: number; 208 public c: number; 209 210 constructor(c: number) { 211 this.id = NextID++; 212 this.c = c; 213 } 214} 215 216@Component 217struct CounterView { 218 @ObjectLink counter: MyCounter; 219 label: string = 'CounterView'; 220 221 build() { 222 Button(`CounterView [${this.label}] this.counter.c=${this.counter.c} +1`) 223 .width(200).height(50) 224 .onClick(() => { 225 this.counter.c += 1; 226 }) 227 } 228} 229 230@Entry 231@Component 232struct MainView { 233 @State firstIndex: number = 0; 234 @State counters: Array<MyCounter> = [new MyCounter(0), new MyCounter(0), new MyCounter(0), 235 new MyCounter(0), new MyCounter(0)]; 236 237 build() { 238 Column() { 239 ForEach(this.counters.slice(this.firstIndex, this.firstIndex + 3), 240 (item: MyCounter) => { 241 CounterView({ label: `Counter item #${item.id}`, counter: item }) 242 }, 243 (item: MyCounter) => item.id.toString() 244 ) 245 Button(`Counters: shift up`) 246 .width(200).height(50) 247 .onClick(() => { 248 this.firstIndex = Math.min(this.firstIndex + 1, this.counters.length - 3); 249 }) 250 Button(`counters: shift down`) 251 .width(200).height(50) 252 .onClick(() => { 253 this.firstIndex = Math.max(0, this.firstIndex - 1); 254 }) 255 } 256 } 257} 258``` 259 260当增加firstIndex的值时,Mainview内的ForEach将更新,并删除与项ID firstIndex-1关联的CounterView子组件。对于ID为firstindex + 3的数组项,将创建新的CounterView子组件实例。由于CounterView子组件的状态变量counter值由父组件Mainview维护,故重建CounterView子组件实例不会重建状态变量counter值。 261 262> **说明:** 263> 264> 违反上述数组项ID规则是最常见的应用开发错误,尤其是在Array<number>场景下,因为执行过程中很容易添加重复的数字。 265 266 267### ForEach的嵌套使用 268 269允许将ForEach嵌套在同一组件中的另一个ForEach中,但更推荐将组件拆分为两个,每个构造函数只包含一个ForEach。下面为ForEach嵌套使用反例。 270 271 272```ts 273class Month { 274 year: number; 275 month: number; 276 days: number[]; 277 278 constructor(year: number, month: number, ...days: number[]) { 279 this.year = year; 280 this.month = month; 281 this.days = days; 282 } 283} 284@Component 285struct CalendarExample { 286 // 模拟6个月 287 arr28: Array<number> = Array(31).fill(0).map((_: number, i: number): number => i + 1); 288 arr30: Array<number> = Array(31).fill(0).map((_: number, i: number): number => i + 1); 289 arr31: Array<number> = Array(31).fill(0).map((_: number, i: number): number => i + 1); 290 @State calendar : Month[] = [ 291 new Month(2020, 1, ...(this.arr31)), 292 new Month(2020, 2, ...(this.arr28)), 293 new Month(2020, 3, ...(this.arr31)), 294 new Month(2020, 4, ...(this.arr30)), 295 new Month(2020, 5, ...(this.arr31)), 296 new Month(2020, 6, ...(this.arr30)) 297 ] 298 build() { 299 Column() { 300 Button() { 301 Text('next month') 302 }.onClick(() => { 303 this.calendar.shift() 304 this.calendar.push(new Month(2020, 7, ...(this.arr31))) 305 }) 306 ForEach(this.calendar, 307 (item: Month) => { 308 ForEach(item.days, 309 (day : number) => { 310 // 构建日期块 311 }, 312 (day : number) => day.toString() 313 )// 内部ForEach 314 }, 315 (item: Month) => (item.year * 12 + item.month).toString() // 字段与年和月一起使用,作为月份的唯一ID。 316 )// 外部ForEach 317 } 318 } 319} 320``` 321 322以上示例存在两个问题: 323 3241. 代码可读性差。 325 3262. 对于上述的年月份数据的数组结构形式,由于框架无法观察到针对该数组中Month数据结构的改变(比如day数组变化),从而内层的ForEach无法刷新日期显示。 327 328建议应用设计时将Calendar拆分为Year、Month和Day子组件。定义一个“Day”模型类,以保存有关day的信息,并用\@Observed装饰此类。DayView组件利用ObjectLink装饰变量以绑定day数据。对MonthView和Month模型类执行同样的操作。 329 330 331### ForEach中使用可选index参数示例 332 333可以在构造函数和ID生成函数中使用可选的index参数。 334 335 336```ts 337@Entry 338@Component 339struct ForEachWithIndex { 340 @State arr: number[] = [4, 3, 1, 5]; 341 342 build() { 343 Column() { 344 ForEach(this.arr, 345 (it: number, index) => { 346 Text(`Item: ${index} - ${it}`) 347 }, 348 (it: number, index) => { 349 return `${index} - ${it}` 350 } 351 ) 352 } 353 } 354} 355``` 356 357必须正确构造ID生成函数。当在项构造函数中使用index参数时,ID生成函数也必须使用index参数,以生成唯一ID和给定源数组项的ID。当数组项在数组中的索引位置发生变化时,其ID会发生变化。 358 359此示例还说明了index参数会造成显著性能下降。即使项在源数组中移动而不做修改,因为索引发生改变,依赖该数组项的UI仍然需要重新渲染。例如,使用索引排序时,数组只需要将ForEach未修改的子UI节点移动到正确的位置,这对于框架来说是一个轻量级操作。而使用索引时,所有子UI节点都需要重新构建,这操作负担要重得多。 360