• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Repeat:可复用的循环渲染
2<!--Kit: ArkUI-->
3<!--Subsystem: ArkUI-->
4<!--Owner: @liubihao-->
5<!--Designer: @keerecles-->
6<!--Tester: @TerryTsao-->
7<!--Adviser: @zhang_yixin13-->
8
9> **说明:**
10>
11> Repeat从API version 12开始支持。
12>
13> 本文档仅为开发指南。组件接口规范见[Repeat API参数说明](../../reference/apis-arkui/arkui-ts/ts-rendering-control-repeat.md)。
14
15## 概述
16
17Repeat基于数组类型数据来进行循环渲染,一般与容器组件配合使用。
18
19Repeat根据容器组件的**有效加载范围**(屏幕可视区域+预加载区域)加载子组件。当容器滑动/数组改变时,Repeat会根据父容器组件的布局过程重新计算有效加载范围,并管理列表子组件节点的创建与销毁。Repeat通过组件节点更新/复用从而优化性能表现,详细描述见[节点更新/复用能力说明](#节点更新复用能力说明)。
20
21> **说明:**
22>
23> Repeat与[LazyForEach](./arkts-rendering-control-lazyforeach.md)组件的区别:
24> - Repeat直接监听状态变量的变化,而LazyForEach需要开发者实现[IDataSource](../../reference/apis-arkui/arkui-ts/ts-rendering-control-lazyforeach.md#idatasource)接口,手动管理子组件内容/索引的修改。
25> - Repeat还增强了节点复用能力,提高了长列表滑动和数据更新的渲染性能。
26> - Repeat增加了渲染模板(template)的能力,在同一个数组中,根据开发者自定义的模板类型(template type)渲染不同的子组件。
27
28## 使用限制
29
30- Repeat必须在滚动类容器组件内使用,仅有[List](../../reference/apis-arkui/arkui-ts/ts-container-list.md)、[ListItemGroup](../../reference/apis-arkui/arkui-ts/ts-container-listitemgroup.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)组件支持Repeat懒加载场景。
31<br/>循环渲染只允许创建一个子组件,子组件应当是允许包含在容器组件中的子组件。例如:Repeat与[List](../../reference/apis-arkui/arkui-ts/ts-container-list.md)组件配合使用时,子组件必须为[ListItem](../../reference/apis-arkui/arkui-ts/ts-container-listitem.md)组件。
32- Repeat不支持V1装饰器,混用V1装饰器会导致渲染异常。
33- Repeat当前不支持动画效果。
34- 滚动容器组件内只能包含一个Repeat。以List为例,同时包含ListItem、ForEach、LazyForEach的场景是不推荐的;同时包含多个Repeat也是不推荐的。
35- 当Repeat与自定义组件或[@Builder](./arkts-builder.md)函数混用时,必须将RepeatItem类型整体进行传参,组件才能监听到数据变化。详见[Repeat与@Builder混用](#repeat与builder混用)。
36
37> **注意:**
38>
39> Repeat功能依赖数组属性的动态修改。如果数组对象被密封(sealed)或冻结(frozen),将导致Repeat部分功能失效,因为密封操作会禁止对象扩展属性并锁定现有属性的配置。
40>
41> 常见触发场景:<br>1)可观察数据的转换:使用[makeObserved](../../reference/apis-arkui/js-apis-StateManagement.md#makeobserved)将普通数组(如[collections.Array](../../reference/apis-arkts/arkts-apis-arkts-collections-Array.md))转换为可观察数据时,某些实现会自动密封数组。<br>2)主动对象保护:显式调用`Object.seal()`或`Object.freeze()`防止数组被修改。
42
43## 循环渲染能力说明
44
45Repeat子组件由`.each()`和`.template()`属性定义,只允许包含一个子组件。当页面首次渲染时,Repeat根据当前的有效加载范围(屏幕可视区域+预加载区域)按需创建子组件。如下图所示:
46
47![Repeat-Render](./figures/Repeat-Render.png)
48
49`.each()`适用于只需要循环渲染一种子组件的场景。下列示例代码使用Repeat组件进行简单的循环渲染。
50
51```ts
52// 在List容器组件中使用Repeat
53@Entry
54@ComponentV2 // 推荐使用V2装饰器
55struct RepeatExample {
56  @Local dataArr: Array<string> = []; // 数据源
57
58  aboutToAppear(): void {
59    for (let i = 0; i < 50; i++) {
60      this.dataArr.push(`data_${i}`); // 为数组添加一些数据
61    }
62  }
63
64  build() {
65    Column() {
66      List() {
67        Repeat<string>(this.dataArr)
68          .each((ri: RepeatItem<string>) => {
69            ListItem() {
70              Text('each_' + ri.item).fontSize(30)
71            }
72          })
73          .virtualScroll({ totalCount: this.dataArr.length }) // 打开懒加载,totalCount为期望加载的数据长度
74      }
75      .cachedCount(2) // 容器组件的预加载区域大小
76      .height('70%')
77      .border({ width: 1 }) // 边框
78    }
79  }
80}
81```
82
83运行后界面如下图所示:
84
85![Repeat-Example-With-Each](./figures/Repeat-Example-With-Each.png)
86
87Repeat提供渲染模板(template)能力,可以在同一个数据源中渲染多种子组件。每个数据项会根据`.templateId()`得到template type,从而渲染type对应的`.template()`中的子组件。
88
89- `.each()`等价于template type为空字符串的`.template()`。
90- 当多个template type相同时(包括template type为空字符串),Repeat仅生效最新定义的`.each()`或`.template()`。
91- 如果`.templateId()`缺省,或`templateId()`计算得到的template type不存在,则template type取默认值空字符串。
92- 只有相同template type的节点可以互相复用。
93
94下列示例代码中使用Repeat组件进行循环渲染,并使用了多个渲染模板。
95
96```ts
97// 在List容器组件中使用Repeat
98@Entry
99@ComponentV2 // 推荐使用V2装饰器
100struct RepeatExampleWithTemplates {
101  @Local dataArr: Array<string> = []; // 数据源
102
103  aboutToAppear(): void {
104    for (let i = 0; i < 50; i++) {
105      this.dataArr.push(`data_${i}`); // 为数组添加一些数据
106    }
107  }
108
109  build() {
110    Column() {
111      List() {
112        Repeat<string>(this.dataArr)
113          .each((ri: RepeatItem<string>) => { // 默认渲染模板
114            ListItem() {
115              Text('each_' + ri.item).fontSize(30).fontColor('rgb(161,10,33)') // 文本颜色为红色
116            }
117          })
118          .key((item: string, index: number): string => JSON.stringify(item)) // 键值生成函数
119          .virtualScroll({ totalCount: this.dataArr.length }) // 打开懒加载,totalCount为期望加载的数据长度
120          .templateId((item: string, index: number): string => { // 根据返回值寻找对应的模板子组件进行渲染
121            return index <= 4 ? 'A' : (index <= 10 ? 'B' : ''); // 前5个节点模板为A,接下来的5个为B,其余为默认模板
122          })
123          .template('A', (ri: RepeatItem<string>) => { // 'A'模板
124            ListItem() {
125              Text('A_' + ri.item).fontSize(30).fontColor('rgb(23,169,141)') // 文本颜色为绿色
126            }
127          }, { cachedCount: 3 }) // 'A'模板的缓存列表容量为3
128          .template('B', (ri: RepeatItem<string>) => { // 'B'模板
129            ListItem() {
130              Text('B_' + ri.item).fontSize(30).fontColor('rgb(39,135,217)') // 文本颜色为蓝色
131            }
132          }, { cachedCount: 4 }) // 'B'模板的缓存列表容量为4
133      }
134      .cachedCount(2) // 容器组件的预加载区域大小
135      .height('70%')
136      .border({ width: 1 }) // 边框
137    }
138  }
139}
140```
141
142运行后界面如下图所示:
143
144![Repeat-Example-With-Templates](./figures/Repeat-Example-With-Templates.png)
145
146## 节点更新/复用能力说明
147
148> **说明:**
149>
150> Repeat子组件的节点操作分为四种:节点创建、节点更新、节点复用、节点销毁。其中,节点更新和节点复用的区别为:
151>
152> - 节点更新:节点不销毁,状态变量驱动节点属性更新。
153> - 节点复用:旧节点不销毁,存储在空闲节点缓存池;需要创建新节点时,直接从缓存池中获取可复用的旧节点,并做相应的节点属性更新。
154
155当**滚动容器组件滑动/数组改变**时,Repeat将失效的子组件节点(离开有效加载范围)加入空闲节点缓存池中,即断开组件节点与页面组件树的连接但不销毁节点。在需要生成新的组件时,对缓存池里的组件节点进行复用。
156
157Repeat组件默认开启节点复用功能。从API version 18开始,可以通过配置`reusable`字段选择是否启用复用功能。为了提高渲染性能,建议开发者保持节点复用。代码示例见[VirtualScrollOptions](../../reference/apis-arkui/arkui-ts/ts-rendering-control-repeat.md#virtualscrolloptions)。
158
159从API version 18开始,Repeat支持L2缓存自定义组件冻结。详细描述见[缓存池自定义组件冻结](./arkts-custom-components-freezeV2.md#repeat)。
160
161下面通过典型的[滑动场景](#滑动场景)和[数据更新场景](#数据更新场景)示例来展示Repeat子组件的渲染逻辑。图中L1缓存为Repeat有效加载区域,L2缓存为每个循环渲染模板的空闲节点缓存池。
162
163定义长度为20的数组,数组前5项的template type为`aa`,其余项为`bb`。`aa`缓存池容量为3,`bb`缓存池容量为4。容器组件的预加载区域大小为2。为了便于理解,在`aa`和`bb`缓存池中分别加入一个和两个空闲节点。
164
165首次渲染,列表的节点状态如下图所示。
166
167![Repeat-Start](./figures/Repeat-Start.PNG)
168
169### 滑动场景
170
171将屏幕向右滑动(屏幕内容右移)一个节点的距离,Repeat将开始复用缓存池中的节点。index=10的节点进入有效加载范围,计算出其template type为`bb`。由于`bb`缓存池非空,Repeat会从`bb`缓存池中取出一个空闲节点进行复用,更新其节点属性,该子组件中涉及数据item和索引index的其他孙子组件会根据V2状态管理的规则做同步更新。其他节点仍在有效加载范围,均只更新索引index。
172
173index=0的节点滑出了有效加载范围。当UI主线程空闲时,会检查`aa`缓存池是否已满,此时`aa`缓存池未满,将该节点加入到对应的缓存池中。
174
175如果此时对应template type的缓存池已满,Repeat会销毁掉多余的节点。
176
177![Repeat-Slide](./figures/Repeat-Slide.PNG)
178
179### 数据更新场景
180
181在上一小节的基础上做如下的数组更新操作,删除index=4的节点,修改节点数据`item_7`为`new_7`。
182
183首先,删除index=4的节点后,失效节点加入`aa`缓存池。后面的列表节点前移,新进入有效加载区域的节点`item_11`会复用`bb`缓存池中的空闲节点,其他节点均只更新索引index。如下图所示。
184
185![Repeat-Update1](./figures/Repeat-Update1.PNG)
186
187其次,节点`item_5`前移,索引index更新为4。根据template type的计算规则,节点`item_5`的template type变为`aa`,需要从`aa`缓存池中复用空闲节点,并且将旧节点加入`bb`缓存池。如下图所示。
188
189![Repeat-Update2](./figures/Repeat-Update2.PNG)
190
191## 键值生成函数
192
193Repeat的`.key()`属性为每个子组件生成一个键值。Repeat通过键值识别数组增加、删除哪些数据以及哪些数据改变了位置(索引)。
194
195> **注意:**
196>
197> 键值(key)与索引(index)的区别:键值是数据项的唯一标识符,Repeat根据键值是否发生变化判断数据项是否更新;索引只标识数据项在数组中的位置。
198
199当`.key()`缺省时,Repeat会生成新的随机键值。当发现有重复key时,Repeat会在已有键值的基础上递归生成新的键值,直到没有重复键值。
200
201键值生成函数`.key()`的使用限制:
202
203- 即使数组发生变化,开发者也必须保证键值key唯一。
204- 每次执行`.key()`函数时,使用相同的数据项作为输入,输出必须是一致的。
205- 允许在`.key()`中使用index,但不建议开发者这样做。因为在数据项移动时索引index发生变化的同时key值也会改变,导致Repeat认为数据发生变化,从而触发子组件重新渲染,降低性能表现。
206- 推荐将简单类型数组转换为类对象数组,并添加一个`readonly id`属性,在构造函数中初始化唯一值。
207
208## 数据精准懒加载
209
210当数据源总长度较长,或数据项加载耗时较长时,可使用Repeat数据精准懒加载特性,避免在初始化时加载所有数据。
211
212开发者可以设置`.virtualScroll()`的`totalCount`属性值或`onTotalCount`自定义方法用于计算期望的数据源长度,设置`onLazyLoading`属性实现数据精准懒加载,实现在节点首次渲染时加载对应的数据。详细说明和注意事项见[VirtualScrollOptions](../../reference/apis-arkui/arkui-ts/ts-rendering-control-repeat.md#virtualscrolloptions)。
213
214**示例1**
215
216数据源总长度较长,在首次渲染、滑动屏幕、跳转显示区域时,动态加载对应区域内的数据。
217
218```ts
219@Entry
220@ComponentV2
221struct RepeatLazyLoading {
222  // 假设数据源总长度较长,为1000。初始数组未提供数据。
223  @Local arr: Array<string> = [];
224  scroller: Scroller = new Scroller();
225  build() {
226    Column({ space: 5 }) {
227      // 初始显示位置为index = 100,数据可通过懒加载自动获取。
228      List({ scroller: this.scroller, space: 5, initialIndex: 100 }) {
229        Repeat(this.arr)
230          .virtualScroll({
231            // 期望的数据源总长度为1000。
232            onTotalCount: () => { return 1000; },
233            // 实现数据懒加载。
234            onLazyLoading: (index: number) => { this.arr[index] = index.toString(); }
235          })
236          .each((obj: RepeatItem<string>) => {
237            ListItem() {
238              Row({ space: 5 }) {
239                Text(`${obj.index}: Item_${obj.item}`)
240              }
241            }
242            .height(50)
243          })
244      }
245      .height('80%')
246      .border({ width: 1})
247      // 显示位置跳转至index = 500,数据可通过懒加载自动获取。
248      Button('ScrollToIndex 500')
249        .onClick(() => { this.scroller.scrollToIndex(500); })
250    }
251  }
252}
253```
254
255运行效果:
256
257![Repeat-Lazyloading-1](./figures/repeat-lazyloading-demo1.gif)
258
259**示例2**
260
261数据加载耗时长,在onLazyLoading方法中,首先为数据项创建占位符,再通过异步任务加载数据。
262
263```ts
264@Entry
265@ComponentV2
266struct RepeatLazyLoading {
267  @Local arr: Array<string> = [];
268  build() {
269    Column({ space: 5 }) {
270      List({ space: 5 }) {
271        Repeat(this.arr)
272          .virtualScroll({
273            onTotalCount: () => { return 100; },
274            // 实现数据懒加载。
275            onLazyLoading: (index: number) => {
276              // 创建占位符。
277              this.arr[index] = '';
278              // 模拟高耗时加载过程,通过异步任务加载数据。
279              setTimeout(() => { this.arr[index] = index.toString(); }, 1000);
280            }
281          })
282          .each((obj: RepeatItem<string>) => {
283            ListItem() {
284              Row({ space: 5 }) {
285                Text(`${obj.index}: Item_${obj.item}`)
286              }
287            }
288            .height(50)
289          })
290      }
291      .height('100%')
292      .border({ width: 1})
293    }
294  }
295}
296```
297
298运行效果:
299
300![Repeat-Lazyloading-2](./figures/repeat-lazyloading-demo2.gif)
301
302**示例3**
303
304使用数据懒加载,并配合设置`onTotalCount: () => { return this.arr.length + 1; }`,可实现数据无限懒加载。
305
306> **注意:**
307>
308> - 此场景下,开发者需要提供首屏显示所需的初始数据,并建议设置父容器组件`cachedCount > 0`,否则将会导致渲染异常。
309> - 若与Swiper-Loop模式同时使用,停留在`index = 0`处时,将导致onLazyLoading方法被持续触发,建议避免与Swiper-Loop模式同时使用。
310> - 开发者需要关注内存消耗情况,避免因数据持续加载而导致内存过量消耗。
311
312```ts
313@Entry
314@ComponentV2
315struct RepeatLazyLoading {
316  @Local arr: Array<string> = [];
317  // 提供首屏显示所需的初始数据。
318  aboutToAppear(): void {
319    for (let i = 0; i < 15; i++) {
320      this.arr.push(i.toString());
321    }
322  }
323  build() {
324    Column({ space: 5 }) {
325      List({ space: 5 }) {
326        Repeat(this.arr)
327          .virtualScroll({
328            // 数据无限懒加载。
329            onTotalCount: () => { return this.arr.length + 1; },
330            onLazyLoading: (index: number) => { this.arr[index] = index.toString(); }
331          })
332          .each((obj: RepeatItem<string>) => {
333            ListItem() {
334              Row({ space: 5 }) {
335                Text(`${obj.index}: Item_${obj.item}`)
336              }
337            }
338            .height(50)
339          })
340      }
341      .height('100%')
342      .border({ width: 1})
343      // 建议设置cachedCount > 0。
344      .cachedCount(1)
345    }
346  }
347}
348```
349
350运行效果:
351
352![Repeat-Lazyloading-3](./figures/repeat-lazyloading-demo3.gif)
353
354
355## 拖拽排序
356
357当Repeat在List组件下使用,并且设置了[onMove](../../reference/apis-arkui/arkui-ts/ts-universal-attributes-drag-sorting.md#onmove)事件,Repeat每次迭代都生成一个ListItem时,可以使能拖拽排序。Repeat拖拽排序特性从API version 19开始支持。
358
359> **注意:**
360>
361> - 拖拽排序离手后,如果数据位置发生变化,则会触发onMove事件,上报数据移动原始索引号和目标索引号。<br/>在onMove事件中,需要根据上报的起始索引号和目标索引号修改数据源。数据源修改前后,要保持每个数据的键值不变,只是顺序发生变化,才能保证落位动画正常执行。
362> - 拖拽排序过程中,在离手之前,不允许修改数据源。
363
364示例代码:
365
366```ts
367@Entry
368@ComponentV2
369struct RepeatVirtualScrollOnMove {
370  @Local simpleList: Array<string> = [];
371
372  aboutToAppear(): void {
373    for (let i = 0; i < 100; i++) {
374      this.simpleList.push(`${i}`);
375    }
376  }
377
378  build() {
379    Column() {
380      List() {
381        Repeat<string>(this.simpleList)
382          // 通过设置onMove,使能拖拽排序。
383          .onMove((from: number, to: number) => {
384            let temp = this.simpleList.splice(from, 1);
385            this.simpleList.splice(to, 0, temp[0]);
386          })
387          .each((obj: RepeatItem<string>) => {
388            ListItem() {
389              Text(obj.item)
390                .fontSize(16)
391                .textAlign(TextAlign.Center)
392                .size({height: 100, width: '100%'})
393            }.margin(10)
394            .borderRadius(10)
395            .backgroundColor('#FFFFFFFF')
396          })
397          .key((item: string, index: number) => {
398            return item;
399          })
400          .virtualScroll({ totalCount: this.simpleList.length })
401      }
402      .border({ width: 1 })
403      .backgroundColor('#FFDCDCDC')
404      .width('100%')
405      .height('100%')
406    }
407  }
408}
409```
410
411运行效果:
412
413![Repeat-Drag-Sort](./figures/repeat-drag-sort.gif)
414
415## 前插保持
416
417前插保持,即在显示区域之前插入或删除数据后,保持显示区域的子组件位置不变。
418
419从API version 20开始,仅当父容器组件为List且[maintainVisibleContentPosition](../../reference/apis-arkui/arkui-ts/ts-container-list.md#maintainvisiblecontentposition12)属性设置为true后,在List显示区域之前插入或删除数据时保持List显示区域子组件位置不变。
420
421**示例代码**
422
423```ts
424@Entry
425@ComponentV2
426struct PreInsertDemo {
427  @Local simpleList: Array<string> = [];
428  private cnt: number = 1;
429
430  aboutToAppear(): void {
431    for (let i = 0; i < 30; i++) {
432      this.simpleList.push(`Hello ${this.cnt++}`);
433    }
434  }
435
436  build() {
437    Column() {
438      Row() {
439        Button(`insert #5`)
440          .onClick(() => {
441            this.simpleList.splice(5, 0, `Hello ${this.cnt++}`);
442          })
443        Button(`delete #0`)
444          .onClick(() => {
445            this.simpleList.splice(0, 1);
446          })
447      }
448
449      List({ initialIndex: 5 }) {
450        Repeat<string>(this.simpleList)
451          .each((obj: RepeatItem<string>) => {
452            ListItem() {
453              Row() {
454                Text(`index: ${obj.index}  `)
455                  .fontSize(16)
456                  .fontColor('#70707070')
457                  .textAlign(TextAlign.End)
458                  .size({ height: 100, width: '40%' })
459                Text(`item: ${obj.item}`)
460                  .fontSize(16)
461                  .textAlign(TextAlign.Start)
462                  .size({ height: 100, width: '60%' })
463              }
464            }.margin(10)
465            .borderRadius(10)
466            .backgroundColor('#FFFFFFFF')
467          })
468          .key((item: string, index: number) => item)
469          .virtualScroll({ totalCount: this.simpleList.length })
470      }
471      .maintainVisibleContentPosition(true) // 启用前插保持
472      .border({ width: 1 })
473      .backgroundColor('#FFDCDCDC')
474      .width('100%')
475      .height('100%')
476    }
477  }
478}
479```
480
481示例中,通过点击按钮在显示区域上方插入或删除数据时,显示区域的节点仅index发生改变,对应数据项不变。
482
483运行效果:
484
485![Repeat-pre-insert-preserve](./figures/repeat-pre-insert-preserve.gif)
486
487## 常见使用场景
488
489### 数据展示&操作
490
491下面的代码示例展示了Repeat修改数组的常见操作,包括**插入数据、修改数据、删除数据、交换数据**。点击下拉框选择索引index值,点击相应的按钮即可操作数据项,依次点击两个数据项可以进行交换。
492
493```ts
494@ObservedV2
495class Repeat006Clazz {
496  @Trace message: string = '';
497
498  constructor(message: string) {
499    this.message = message;
500  }
501}
502
503@Entry
504@ComponentV2
505struct RepeatVirtualScroll2T {
506  @Local simpleList: Array<Repeat006Clazz> = [];
507  private exchange: number[] = [];
508  private counter: number = 0;
509  @Local selectOptions: SelectOption[] = [];
510  @Local selectIdx: number = 0;
511
512  @Monitor('simpleList')
513  reloadSelectOptions(): void {
514    this.selectOptions = [];
515    for (let i = 0; i < this.simpleList.length; ++i) {
516      this.selectOptions.push({ value: i.toString() });
517    }
518    if (this.selectIdx >= this.simpleList.length) {
519      this.selectIdx = this.simpleList.length - 1;
520    }
521  }
522
523  aboutToAppear(): void {
524    for (let i = 0; i < 100; i++) {
525      this.simpleList.push(new Repeat006Clazz(`item_${i}`));
526    }
527    this.reloadSelectOptions();
528  }
529
530  handleExchange(idx: number): void { // 点击交换子组件
531    this.exchange.push(idx);
532    if (this.exchange.length === 2) {
533      let _a = this.exchange[0];
534      let _b = this.exchange[1];
535      let temp: Repeat006Clazz = this.simpleList[_a];
536      this.simpleList[_a] = this.simpleList[_b];
537      this.simpleList[_b] = temp;
538      this.exchange = [];
539    }
540  }
541
542  build() {
543    Column({ space: 10 }) {
544      Text('virtualScroll each()&template() 2t')
545        .fontSize(15)
546        .fontColor(Color.Gray)
547      Text('Select an index and press the button to update data.')
548        .fontSize(15)
549        .fontColor(Color.Gray)
550
551      Select(this.selectOptions)
552        .selected(this.selectIdx)
553        .value(this.selectIdx.toString())
554        .key('selectIdx')
555        .onSelect((index: number) => {
556          this.selectIdx = index;
557        })
558      Row({ space: 5 }) {
559        Button('Add No.' + this.selectIdx)
560          .onClick(() => {
561            this.simpleList.splice(this.selectIdx, 0, new Repeat006Clazz(`${this.counter++}_add_item`));
562            this.reloadSelectOptions();
563          })
564        Button('Modify No.' + this.selectIdx)
565          .onClick(() => {
566            this.simpleList.splice(this.selectIdx, 1, new Repeat006Clazz(`${this.counter++}_modify_item`));
567          })
568        Button('Del No.' + this.selectIdx)
569          .onClick(() => {
570            this.simpleList.splice(this.selectIdx, 1);
571            this.reloadSelectOptions();
572          })
573      }
574      Button('Update array length to 5.')
575        .onClick(() => {
576          this.simpleList = this.simpleList.slice(0, 5);
577          this.reloadSelectOptions();
578        })
579
580      Text('Click on two items to exchange.')
581        .fontSize(15)
582        .fontColor(Color.Gray)
583
584      List({ space: 10 }) {
585        Repeat<Repeat006Clazz>(this.simpleList)
586          .each((obj: RepeatItem<Repeat006Clazz>) => {
587            ListItem() {
588              Text(`[each] index${obj.index}: ${obj.item.message}`)
589                .fontSize(25)
590                .onClick(() => {
591                  this.handleExchange(obj.index);
592                })
593            }
594          })
595          .key((item: Repeat006Clazz, index: number) => {
596            return item.message;
597          })
598          .virtualScroll({ totalCount: this.simpleList.length })
599          .templateId((item: Repeat006Clazz, index: number) => {
600            return (index % 2 === 0) ? 'odd' : 'even';
601          })
602          .template('odd', (ri) => {
603            Text(`[odd] index${ri.index}: ${ri.item.message}`)
604              .fontSize(25)
605              .fontColor(Color.Blue)
606              .onClick(() => {
607                this.handleExchange(ri.index);
608              })
609          }, { cachedCount: 3 })
610          .template('even', (ri) => {
611            Text(`[even] index${ri.index}: ${ri.item.message}`)
612              .fontSize(25)
613              .fontColor(Color.Green)
614              .onClick(() => {
615                this.handleExchange(ri.index);
616              })
617          }, { cachedCount: 1 })
618      }
619      .cachedCount(2)
620      .border({ width: 1 })
621      .width('95%')
622      .height('40%')
623    }
624    .justifyContent(FlexAlign.Center)
625    .width('100%')
626    .height('100%')
627  }
628}
629```
630
631该示例代码展示了100项自定义类`RepeatClazz`的`message`字符串属性,List组件的cachedCount属性设为2,模板'odd'和'even'的空闲节点缓存池大小分别设为3和1。运行后界面如下图所示:
632
633![Repeat-VirtualScroll-2T-Demo](./figures/Repeat-VirtualScroll-2T-Demo.gif)
634
635### Repeat嵌套
636
637Repeat支持嵌套使用,示例代码如下:
638
639```ts
640// Repeat嵌套
641@Entry
642@ComponentV2
643struct RepeatNest {
644  @Local outerList: string[] = [];
645  @Local innerList: number[] = [];
646
647  aboutToAppear(): void {
648    for (let i = 0; i < 20; i++) {
649      this.outerList.push(i.toString());
650      this.innerList.push(i);
651    }
652  }
653
654  build() {
655    Column({ space: 20 }) {
656      Text('Repeat virtualScroll嵌套')
657        .fontSize(15)
658        .fontColor(Color.Gray)
659      List() {
660        Repeat<string>(this.outerList)
661          .each((obj) => {
662            ListItem() {
663              Column() {
664                Text('outerList item: ' + obj.item)
665                  .fontSize(30)
666                List() {
667                  Repeat<number>(this.innerList)
668                    .each((subObj) => {
669                      ListItem() {
670                        Text('innerList item: ' + subObj.item)
671                          .fontSize(20)
672                      }
673                    })
674                    .key((item) => 'innerList_' + item)
675                    .virtualScroll()
676                }
677                .width('80%')
678                .border({ width: 1 })
679                .backgroundColor(Color.Orange)
680              }
681              .height('30%')
682              .backgroundColor(Color.Pink)
683            }
684            .border({ width: 1 })
685          })
686          .key((item) => 'outerList_' + item)
687          .virtualScroll()
688      }
689      .width('80%')
690      .border({ width: 1 })
691    }
692    .justifyContent(FlexAlign.Center)
693    .width('90%')
694    .height('80%')
695  }
696}
697```
698
699运行效果:
700
701![Repeat-Nest](./figures/Repeat-Nest.png)
702
703### 父容器组件应用场景
704
705本节展示Repeat与滚动容器组件的常见应用场景。
706
707**与List组合使用**
708
709在List容器组件中使用Repeat,示例代码如下:
710
711```ts
712class DemoListItemInfo {
713  name: string;
714  icon: Resource;
715
716  constructor(name: string, icon: Resource) {
717    this.name = name;
718    this.icon = icon;
719  }
720}
721
722@Entry
723@ComponentV2
724struct DemoList {
725  @Local videoList: Array<DemoListItemInfo> = [];
726
727  aboutToAppear(): void {
728    for (let i = 0; i < 10; i++) {
729      // 此处app.media.listItem0app.media.listItem1app.media.listItem2仅作示例,请开发者自行替换
730      this.videoList.push(new DemoListItemInfo('视频' + i,
731        i % 3 == 0 ? $r('app.media.listItem0') :
732        i % 3 == 1 ? $r('app.media.listItem1') : $r('app.media.listItem2')));
733    }
734  }
735
736  @Builder
737  itemEnd(index: number) {
738    Button('删除')
739      .backgroundColor(Color.Red)
740      .onClick(() => {
741        this.videoList.splice(index, 1);
742      })
743  }
744
745  build() {
746    Column({ space: 10 }) {
747      Text('List容器组件中包含Repeat组件')
748        .fontSize(15)
749        .fontColor(Color.Gray)
750
751      List({ space: 5 }) {
752        Repeat<DemoListItemInfo>(this.videoList)
753          .each((obj: RepeatItem<DemoListItemInfo>) => {
754            ListItem() {
755              Column() {
756                Image(obj.item.icon)
757                  .width('80%')
758                  .margin(10)
759                Text(obj.item.name)
760                  .fontSize(20)
761              }
762            }
763            .swipeAction({
764              end: {
765                builder: () => {
766                  this.itemEnd(obj.index);
767                }
768              }
769            })
770            .onAppear(() => {
771              console.info('AceTag', obj.item.name);
772            })
773          })
774          .key((item: DemoListItemInfo) => item.name)
775          .virtualScroll()
776      }
777      .cachedCount(2)
778      .height('90%')
779      .border({ width: 1 })
780      .listDirection(Axis.Vertical)
781      .alignListItem(ListItemAlign.Center)
782      .divider({
783        strokeWidth: 1,
784        startMargin: 60,
785        endMargin: 60,
786        color: '#ffe9f0f0'
787      })
788
789      Row({ space: 10 }) {
790        Button('删除第1项')
791          .onClick(() => {
792            this.videoList.splice(0, 1);
793          })
794        Button('删除第5项')
795          .onClick(() => {
796            this.videoList.splice(4, 1);
797          })
798      }
799    }
800    .width('100%')
801    .height('100%')
802    .justifyContent(FlexAlign.Center)
803  }
804}
805```
806
807右滑并点击按钮,或点击底部按钮,可删除视频卡片:
808
809![Repeat-Demo-List](./figures/Repeat-Demo-List.gif)
810
811**与Grid组合使用**
812
813在Grid容器组件中使用Repeat,示例如下:
814
815```ts
816class DemoGridItemInfo {
817  name: string;
818  icon: Resource;
819
820  constructor(name: string, icon: Resource) {
821    this.name = name;
822    this.icon = icon;
823  }
824}
825
826@Entry
827@ComponentV2
828struct DemoGrid {
829  @Local itemList: Array<DemoGridItemInfo> = [];
830  @Local isRefreshing: boolean = false;
831  private layoutOptions: GridLayoutOptions = {
832    regularSize: [1, 1],
833    irregularIndexes: [10]
834  };
835  private gridScroller: Scroller = new Scroller();
836  private num: number = 0;
837
838  aboutToAppear(): void {
839    for (let i = 0; i < 10; i++) {
840      // 此处app.media.gridItem0app.media.gridItem1app.media.gridItem2仅作示例,请开发者自行替换
841      this.itemList.push(new DemoGridItemInfo('视频' + i,
842        i % 3 == 0 ? $r('app.media.gridItem0') :
843        i % 3 == 1 ? $r('app.media.gridItem1') : $r('app.media.gridItem2')));
844    }
845  }
846
847  build() {
848    Column({ space: 10 }) {
849      Text('Grid容器组件中包含Repeat组件')
850        .fontSize(15)
851        .fontColor(Color.Gray)
852
853      Refresh({ refreshing: $$this.isRefreshing }) {
854        Grid(this.gridScroller, this.layoutOptions) {
855          Repeat<DemoGridItemInfo>(this.itemList)
856            .each((obj: RepeatItem<DemoGridItemInfo>) => {
857              if (obj.index === 10 ) {
858                GridItem() {
859                  Text('先前浏览至此,点击刷新')
860                    .fontSize(20)
861                }
862                .height(30)
863                .border({ width: 1 })
864                .onClick(() => {
865                  this.gridScroller.scrollToIndex(0);
866                  this.isRefreshing = true;
867                })
868                .onAppear(() => {
869                  console.info('AceTag', obj.item.name);
870                })
871              } else {
872                GridItem() {
873                  Column() {
874                    Image(obj.item.icon)
875                      .width('100%')
876                      .height(80)
877                      .objectFit(ImageFit.Cover)
878                      .borderRadius({ topLeft: 16, topRight: 16 })
879                    Text(obj.item.name)
880                      .fontSize(15)
881                      .height(20)
882                  }
883                }
884                .height(100)
885                .borderRadius(16)
886                .backgroundColor(Color.White)
887                .onAppear(() => {
888                  console.info('AceTag', obj.item.name);
889                })
890              }
891            })
892            .key((item: DemoGridItemInfo) => item.name)
893            .virtualScroll()
894        }
895        .columnsTemplate('repeat(auto-fit, 150)')
896        .cachedCount(4)
897        .rowsGap(15)
898        .columnsGap(10)
899        .height('100%')
900        .padding(10)
901        .backgroundColor('#F1F3F5')
902      }
903      .onRefreshing(() => {
904        setTimeout(() => {
905          this.itemList.splice(10, 1);
906          this.itemList.unshift(new DemoGridItemInfo('refresh', $r('app.media.gridItem0'))); // 此处app.media.gridItem0仅作示例,请开发者自行替换
907          for (let i = 0; i < 10; i++) {
908            // 此处app.media.gridItem0app.media.gridItem1app.media.gridItem2仅作示例,请开发者自行替换
909            this.itemList.unshift(new DemoGridItemInfo('新视频' + this.num,
910              i % 3 == 0 ? $r('app.media.gridItem0') :
911              i % 3 == 1 ? $r('app.media.gridItem1') : $r('app.media.gridItem2')));
912            this.num++;
913          }
914          this.isRefreshing = false;
915        }, 1000);
916        console.info('AceTag', 'onRefreshing');
917      })
918      .refreshOffset(64)
919      .pullToRefresh(true)
920      .width('100%')
921      .height('85%')
922
923      Button('刷新')
924        .onClick(() => {
925          this.gridScroller.scrollToIndex(0);
926          this.isRefreshing = true;
927        })
928    }
929    .width('100%')
930    .height('100%')
931    .justifyContent(FlexAlign.Center)
932  }
933}
934```
935
936下拉屏幕,或点击刷新按钮,或点击“先前浏览至此,点击刷新”,可加载新的视频内容:
937
938![Repeat-Demo-Grid](./figures/Repeat-Demo-Grid.gif)
939
940**与Swiper组合使用**
941
942在Swiper容器组件中使用Repeat,示例如下:
943
944```ts
945const remotePictures: Array<string> = [
946  'https://www.example.com/xxx/0001.jpg', // 请填写具体的网络图片地址
947  'https://www.example.com/xxx/0002.jpg',
948  'https://www.example.com/xxx/0003.jpg',
949  'https://www.example.com/xxx/0004.jpg',
950  'https://www.example.com/xxx/0005.jpg',
951  'https://www.example.com/xxx/0006.jpg',
952  'https://www.example.com/xxx/0007.jpg',
953  'https://www.example.com/xxx/0008.jpg',
954  'https://www.example.com/xxx/0009.jpg'
955];
956
957@ObservedV2
958class DemoSwiperItemInfo {
959  id: string;
960  @Trace url: string = 'default';
961
962  constructor(id: string) {
963    this.id = id;
964  }
965}
966
967@Entry
968@ComponentV2
969struct DemoSwiper {
970  @Local pics: Array<DemoSwiperItemInfo> = [];
971
972  aboutToAppear(): void {
973    for (let i = 0; i < 9; i++) {
974      this.pics.push(new DemoSwiperItemInfo('pic' + i));
975    }
976    setTimeout(() => {
977      this.pics[0].url = remotePictures[0];
978    }, 1000);
979  }
980
981  build() {
982    Column() {
983      Text('Swiper容器组件中包含Repeat组件')
984        .fontSize(15)
985        .fontColor(Color.Gray)
986
987      Stack() {
988        Text('图片加载中')
989          .fontSize(15)
990          .fontColor(Color.Gray)
991        Swiper() {
992          Repeat(this.pics)
993            .each((obj: RepeatItem<DemoSwiperItemInfo>) => {
994              Image(obj.item.url)
995                .onAppear(() => {
996                  console.info('AceTag', obj.item.id);
997                })
998            })
999            .key((item: DemoSwiperItemInfo) => item.id)
1000            .virtualScroll()
1001        }
1002        .cachedCount(9)
1003        .height('50%')
1004        .loop(false)
1005        .indicator(true)
1006        .onChange((index) => {
1007          setTimeout(() => {
1008            this.pics[index].url = remotePictures[index];
1009          }, 1000);
1010        })
1011      }
1012      .width('100%')
1013      .height('100%')
1014      .backgroundColor(Color.Black)
1015    }
1016  }
1017}
1018```
1019
1020定时1秒后加载图片,模拟网络延迟:
1021
1022![Repeat-Demo-Swiper](./figures/Repeat-Demo-Swiper.gif)
1023
1024## 关闭懒加载
1025
1026当关闭Repeat的`.virtualScroll()`属性时(即省略该属性),Repeat在初始化页面时加载列表中的所有子组件,适合**短数据列表/组件全部加载**的场景。对于**长数据列表(数据长度大于30)**,如果关闭懒加载,Repeat会一次性加载全量子组件,此操作耗时长,不建议使用。
1027
1028> **注意:**
1029>
1030> - 渲染模板特性(template)不可用。
1031> - 不受滚动容器组件的限制,可以在任意场景使用。
1032> - 支持与V1装饰器混用。
1033> - 页面刷新取决于键值变化:如果键值相同,即使数据改变,页面也不会刷新。详见[节点更新能力说明](#节点更新能力说明)。
1034
1035### 节点更新能力说明
1036
1037(关闭懒加载后)页面首次渲染时,Repeat子组件全部创建。数组发生改变后,Repeat对子组件节点的处理分为以下几个步骤:
1038
1039首先,遍历旧数组键值。如果新数组中没有该键值,将其加入键值集合deletedKeys。
1040
1041其次,遍历新数组键值。依次判断以下条件,进行符合条件的操作:
1042
10431. 若在旧数组中能找到相同键值,直接使用对应的子组件节点,并更新索引index。
10442. 若deletedKeys非空,按照先进后出的顺序,更新该集合中的键值所对应的节点。
10453. 若deletedKeys为空,则表示没有可以更新的节点,需要创建新节点。
1046
1047最后,如果新数组键值遍历结束后,deletedKeys非空,则销毁集合中的键值所对应的节点。
1048
1049![Repeat-NonVS-FuncGen](./figures/Repeat-NonVS-FuncGen.png)
1050
1051以下图中的数组变化为例,图中的`item_X`表示数据项的键值key。
1052
1053![Repeat-NonVS-Example](./figures/Repeat-NonVS-Example.png)
1054
1055根据上述判断逻辑:`item_0`没有变化,`item_1`和`item_2`只更新了索引,`item_n1`和`item_n2`分别由`item_4`和`item_3`进行节点更新获得,`item_n3`为新创建的节点。
1056
1057> **说明:**
1058>
1059> Repeat关闭懒加载场景与[ForEach](arkts-rendering-control-foreach.md)组件的区别:
1060> - 针对特定数组更新场景的渲染性能进行了优化
1061> - 将子组件的内容/索引管理职责转移至框架层面
1062
1063### 示例
1064
1065```ts
1066@Entry
1067@ComponentV2
1068struct Parent {
1069  @Local simpleList: Array<string> = ['one', 'two', 'three'];
1070
1071  build() {
1072    Row() {
1073      Column() {
1074        Text('点击修改第3个数组项的值')
1075          .fontSize(24)
1076          .fontColor(Color.Red)
1077          .onClick(() => {
1078            this.simpleList[2] = 'new three';
1079          })
1080
1081        Repeat<string>(this.simpleList)
1082            .each((obj: RepeatItem<string>)=>{
1083              ChildItem({ item: obj.item })
1084                .margin({top: 20})
1085            })
1086            .key((item: string) => item)
1087      }
1088      .justifyContent(FlexAlign.Center)
1089      .width('100%')
1090      .height('100%')
1091    }
1092    .height('100%')
1093    .backgroundColor(0xF1F3F5)
1094  }
1095}
1096
1097@ComponentV2
1098struct ChildItem {
1099  @Param @Require item: string;
1100
1101  build() {
1102    Text(this.item)
1103      .fontSize(30)
1104  }
1105}
1106```
1107
1108![ForEach-Non-Initial-Render-Case-Effect](./figures/ForEach-Non-Initial-Render-Case-Effect.gif)
1109
1110点击红色字体,第三个数据项发生变化(直接使用旧的组件节点,仅刷新数据)。
1111
1112## 常见问题
1113
1114### 屏幕外的列表数据发生变化时,保证滚动条位置不变
1115
1116以下示例中,屏幕外的数据源变化将影响屏幕中List列表Scroller停留的位置:
1117在List组件中声明Repeat组件,实现key值生成逻辑和each逻辑(如下示例代码),点击按钮“insert”,在屏幕显示的第一个元素前面插入一个元素,屏幕出现向下滚动。
1118
1119```ts
1120// 定义一个类,标记为可观察的
1121// 类中自定义一个数组,标记为可追踪的
1122@ObservedV2
1123class ArrayHolder {
1124  @Trace arr: Array<number> = [];
1125
1126  // constructor,用于初始化数组个数
1127  constructor(count: number) {
1128    for (let i = 0; i < count; i++) {
1129      this.arr.push(i);
1130    }
1131  }
1132}
1133
1134@Entry
1135@ComponentV2
1136struct RepeatTemplateSingle {
1137  @Local arrayHolder: ArrayHolder = new ArrayHolder(100);
1138  @Local totalCount: number = this.arrayHolder.arr.length;
1139  scroller: Scroller = new Scroller();
1140
1141  build() {
1142    Column({ space: 5 }) {
1143      List({ space: 20, initialIndex: 19, scroller: this.scroller }) {
1144        Repeat(this.arrayHolder.arr)
1145          .virtualScroll({ totalCount: this.totalCount })
1146          .templateId((item, index) => {
1147            return 'number';
1148          })
1149          .template('number', (r) => {
1150            ListItem() {
1151              Text(r.index! + ':' + r.item + 'Reuse');
1152            }
1153          })
1154          .each((r) => {
1155            ListItem() {
1156              Text(r.index! + ':' + r.item + 'eachMessage');
1157            }
1158          })
1159      }
1160      .height('30%')
1161
1162      Button(`insert totalCount ${this.totalCount}`)
1163        .height(60)
1164        .onClick(() => {
1165          // 插入元素,元素位置为屏幕显示的前一个元素
1166          this.arrayHolder.arr.splice(18, 0, this.totalCount);
1167          this.totalCount = this.arrayHolder.arr.length;
1168        })
1169    }
1170    .width('100%')
1171    .margin({ top: 5 })
1172  }
1173}
1174```
1175
1176运行效果:
1177
1178![repeat-case1-wrong](./figures/repeat-case1-wrong.gif)
1179
1180以下为修正后的示例:
1181在一些场景中,我们不希望屏幕外的数据源变化影响屏幕中List列表Scroller停留的位置,可以通过List组件的[onScrollIndex](../arkts-layout-development-create-list.md#响应滚动位置)事件对列表滚动动作进行监听,当列表发生滚动时,获取列表滚动位置。使用Scroller组件的[scrollToIndex](../../reference/apis-arkui/arkui-ts/ts-container-scroll.md#scrolltoindex)特性,滑动到指定index位置,实现屏幕外的数据源增加/删除数据时,Scroller停留的位置不变的效果。
1182
1183示例代码仅对增加数据的情况进行展示。
1184
1185```ts
1186// ...ArrayHolder的定义和上述demo代码一致
1187
1188@Entry
1189@ComponentV2
1190struct RepeatTemplateSingle {
1191  @Local arrayHolder: ArrayHolder = new ArrayHolder(100);
1192  @Local totalCount: number = this.arrayHolder.arr.length;
1193  scroller: Scroller = new Scroller();
1194
1195  private start: number = 1;
1196  private end: number = 1;
1197
1198  build() {
1199    Column({ space: 5 }) {
1200      List({ space: 20, initialIndex: 19, scroller: this.scroller }) {
1201        Repeat(this.arrayHolder.arr)
1202          .virtualScroll({ totalCount: this.totalCount })
1203          .templateId((item, index) => {
1204            return 'number';
1205          })
1206          .template('number', (r) => {
1207            ListItem() {
1208              Text(r.index! + ':' + r.item + 'Reuse')
1209            }
1210          })
1211          .each((r) => {
1212            ListItem() {
1213              Text(r.index! + ':' + r.item + 'eachMessage')
1214            }
1215          })
1216      }
1217      .onScrollIndex((start, end) => {
1218        this.start = start;
1219        this.end = end;
1220      })
1221      .height('30%')
1222
1223      Button(`insert totalCount ${this.totalCount}`)
1224        .height(60)
1225        .onClick(() => {
1226          // 插入元素,元素位置为屏幕显示的前一个元素
1227          this.arrayHolder.arr.splice(18, 0, this.totalCount);
1228          let rect = this.scroller.getItemRect(this.start); // 获取子组件的大小位置
1229          this.scroller.scrollToIndex(this.start + 1); // 滑动到指定index
1230          this.scroller.scrollBy(0, -rect.y); // 滑动指定距离
1231          this.totalCount = this.arrayHolder.arr.length;
1232        })
1233    }
1234    .width('100%')
1235    .margin({ top: 5 })
1236  }
1237}
1238```
1239
1240运行效果:
1241
1242![repeat-case1-fixed](./figures/repeat-case1-fixed.gif)
1243
1244### totalCount值大于数据源长度
1245
1246当数据源总长度很大时,会使用懒加载的方式先加载一部分数据,为了使Repeat显示正确的滚动条样式,需要将数据总长度赋值给totalCount,即数据源全部加载完成前,totalCount大于array.length1247
1248totalCount > array.length时,在父组件容器滚动过程中,应用需要保证列表即将滑动到数据源末尾时请求后续数据,开发者需要对数据请求的错误场景(如网络延迟)进行保护操作,直到数据源全部加载完成,否则列表滑动的过程中会出现滚动效果异常。
1249
1250上述规范可以通过实现父组件List/Grid的[onScrollIndex](../arkts-layout-development-create-list.md#响应滚动位置)属性的回调函数完成。示例代码如下:
1251
1252```ts
1253@ObservedV2
1254class VehicleData {
1255  @Trace name: string;
1256  @Trace price: number;
1257
1258  constructor(name: string, price: number) {
1259    this.name = name;
1260    this.price = price;
1261  }
1262}
1263
1264@ObservedV2
1265class VehicleDB {
1266  public vehicleItems: VehicleData[] = [];
1267
1268  constructor() {
1269    // 数组初始化大小 20
1270    for (let i = 1; i <= 20; i++) {
1271      this.vehicleItems.push(new VehicleData(`Vehicle${i}`, i));
1272    }
1273  }
1274}
1275
1276@Entry
1277@ComponentV2
1278struct entryCompSucc {
1279  @Local vehicleItems: VehicleData[] = new VehicleDB().vehicleItems;
1280  @Local listChildrenSize: ChildrenMainSize = new ChildrenMainSize(60);
1281  @Local totalCount: number = this.vehicleItems.length;
1282  scroller: Scroller = new Scroller();
1283
1284  build() {
1285    Column({ space: 3 }) {
1286      List({ scroller: this.scroller }) {
1287        Repeat(this.vehicleItems)
1288          .virtualScroll({ totalCount: 50 }) // 数组预期长度 50
1289          .templateId(() => 'default')
1290          .template('default', (ri) => {
1291            ListItem() {
1292              Column() {
1293                Text(`${ri.item.name} + ${ri.index}`)
1294                  .width('90%')
1295                  .height(this.listChildrenSize.childDefaultSize)
1296                  .backgroundColor(0xFFA07A)
1297                  .textAlign(TextAlign.Center)
1298                  .fontSize(20)
1299                  .fontWeight(FontWeight.Bold)
1300              }
1301            }.border({ width: 1 })
1302          }, { cachedCount: 5 })
1303          .each((ri) => {
1304            ListItem() {
1305              Text('Wrong: ' + `${ri.item.name} + ${ri.index}`)
1306                .width('90%')
1307                .height(this.listChildrenSize.childDefaultSize)
1308                .backgroundColor(0xFFA07A)
1309                .textAlign(TextAlign.Center)
1310                .fontSize(20)
1311                .fontWeight(FontWeight.Bold)
1312            }.border({ width: 1 })
1313          })
1314          .key((item, index) => `${index}:${item}`)
1315      }
1316      .height('50%')
1317      .margin({ top: 20 })
1318      .childrenMainSize(this.listChildrenSize)
1319      .alignListItem(ListItemAlign.Center)
1320      .onScrollIndex((start, end) => {
1321        console.log('onScrollIndex', start, end);
1322        // 数据懒加载
1323        if (this.vehicleItems.length < 50) {
1324          for (let i = 0; i < 10; i++) {
1325            if (this.vehicleItems.length < 50) {
1326              this.vehicleItems.push(new VehicleData('Vehicle_loaded', i));
1327            }
1328          }
1329        }
1330      })
1331    }
1332  }
1333}
1334```
1335
1336示例代码运行效果:
1337
1338![Repeat-Case2-Succ](./figures/Repeat-Case2-Succ.gif)
1339
1340### Repeat与@Builder混用
1341
1342当Repeat与@Builder混用时,必须将RepeatItem类型整体进行传参,组件才能监听到数据变化,如果只传递`RepeatItem.item`或`RepeatItem.index`,将会出现UI渲染异常。
1343
1344示例代码如下:
1345
1346```ts
1347@Entry
1348@ComponentV2
1349struct RepeatBuilderPage {
1350  @Local simpleList1: Array<number> = [];
1351  @Local simpleList2: Array<number> = [];
1352
1353  aboutToAppear(): void {
1354    for (let i = 0; i < 100; i++) {
1355      this.simpleList1.push(i);
1356      this.simpleList2.push(i);
1357    }
1358  }
1359
1360  build() {
1361    Column({ space: 20 }) {
1362      Text('Repeat与@Builder混用,左边是异常场景,右边是正常场景,向下滑动一段距离可以看出差别')
1363        .fontSize(15)
1364        .fontColor(Color.Gray)
1365
1366      Row({ space: 20 }) {
1367        List({ initialIndex: 5, space: 20 }) {
1368          Repeat<number>(this.simpleList1)
1369            .each((ri) => {})
1370            .virtualScroll({ totalCount: this.simpleList1.length })
1371            .templateId((item: number, index: number) => 'default')
1372            .template('default', (ri) => {
1373              ListItem() {
1374                Column() {
1375                  Text('Text id = ' + ri.item)
1376                    .fontSize(20)
1377                  this.buildItem1(ri.item) // 错误示例,为避免渲染异常,应修改为:this.buildItem1(ri)
1378                }
1379              }
1380              .border({ width: 1 })
1381            }, { cachedCount: 3 })
1382        }
1383        .cachedCount(1)
1384        .border({ width: 1 })
1385        .width('45%')
1386        .height('60%')
1387
1388        List({ initialIndex: 5, space: 20 }) {
1389          Repeat<number>(this.simpleList2)
1390            .each((ri) => {})
1391            .virtualScroll({ totalCount: this.simpleList2.length })
1392            .templateId((item: number, index: number) => 'default')
1393            .template('default', (ri) => {
1394              ListItem() {
1395                Column() {
1396                  Text('Text id = ' + ri.item)
1397                    .fontSize(20)
1398                  this.buildItem2(ri) // 正确示例,渲染正常
1399                }
1400              }
1401              .border({ width: 1 })
1402            }, { cachedCount: 3 })
1403        }
1404        .cachedCount(1)
1405        .border({ width: 1 })
1406        .width('45%')
1407        .height('60%')
1408      }
1409    }
1410    .height('100%')
1411    .justifyContent(FlexAlign.Center)
1412  }
1413
1414  @Builder
1415  // @Builder参数必须传RepeatItem类型才能正常渲染
1416  buildItem1(item: number) {
1417    Text('Builder1 id = ' + item)
1418      .fontSize(20)
1419      .fontColor(Color.Red)
1420      .margin({ top: 2 })
1421  }
1422
1423  @Builder
1424  buildItem2(ri: RepeatItem<number>) {
1425    Text('Builder2 id = ' + ri.item)
1426      .fontSize(20)
1427      .fontColor(Color.Red)
1428      .margin({ top: 2 })
1429  }
1430}
1431```
1432
1433界面展示如下图,进入页面后向下滑动一段距离可以看出差别,左边是错误用法,右边是正确用法(Text组件为黑色,Builder组件为红色)。上述代码展示了开发过程中易出错的场景,即在@Builder构造函数中传参方式为值传递。
1434
1435![Repeat-Builder](./figures/Repeat-Builder.png)