• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Repeat:子组件复用
2
3>**说明:**
4>
5>Repeat从API version 12开始支持。
6>
7>当前状态管理(V2试用版)仍在逐步开发中,相关功能尚未成熟,建议开发者尝鲜试用。
8
9API参数说明见:[Repeat API参数说明](../reference/apis-arkui/arkui-ts/ts-rendering-control-repeat.md)
10
11Repeat组件不开启virtualScroll开关时,Repeat基于数组类型数据来进行循环渲染,需要与容器组件配合使用,且接口返回的组件应当是允许包含在Repeat父容器组件中的子组件。Repeat循环渲染和ForEach相比有两个区别,一是优化了部分更新场景下的渲染性能,二是组件生成函数的索引index由框架侧来维护。
12
13Repeat组件开启virtualScroll开关时,Repeat将从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。当在滚动容器中使用了Repeat,框架会根据滚动容器可视区域按需创建组件,当组件滑出可视区域外时,框架会缓存组件,并在下一次迭代中使用。
14
15> **注意:**
16>
17> Repeat组件的virtualScroll场景不完全兼容V1装饰器,使用V1装饰器存在渲染异常,不建议开发者同时使用V1装饰器和virtualScroll场景。
18
19## 使用限制
20
21- 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)组件支持虚拟滚动(此时配置cachedCount会生效)。其它容器组件使用Repeat时请不要打开virtualScroll开关。
22- Repeat开启virtualScroll后,在每次迭代中,必须创建且只允许创建一个子组件。不开启virtualScroll没有该限制。
23- 生成的子组件必须是允许包含在Repeat父容器组件中的子组件。
24- 允许Repeat包含在if/else条件渲染语句中,也允许Repeat中出现if/else条件渲染语句。
25- Repeat内部使用键值作为标识,因此键值生成器必须针对每个数据生成唯一的值,如果多个数据同一时刻生成的键值相同,会导致UI组件渲染出现问题。
26- 未开启virtualScroll目前暂时不支持template模板,复用会有问题。
27- 当Repeat与@Builder混用时,必须将RepeatItem类型整体进行传参,组件才能监听到数据变化,如果只传递RepeatItem.itemRepeatItem.index,将会出现UI渲染异常。
28- virtualScroll场景下,自定义了totalCount值。当数据源长度发生改变时,需要手动更新totalCount值,否则会出现列表显示区域渲染异常。
29
30## 键值生成规则
31
32### non-virtualScroll规则
33
34![Repeat-Slide](./figures/Repeat-NonVirtualScroll-Key.PNG)
35
36### virtualScroll规则
37
38和non-virtualScroll的键值生成规则基本一致,但是不会自动处理重复的键值,需要开发者自己保证键值的唯一性。
39
40![Repeat-Slide](./figures/Repeat-VirtualScroll-Key.PNG)
41
42## 组件生成及复用规则
43
44### non-virtualScroll规则
45
46子组件在Repeat首次渲染时全部创建,在数据更新时会对原组件进行复用。
47
48在Repeat组件进行数据更新时,它会依次对比上次的所有键值和本次更新之后的区别。若当前键值和上次的某一项键值相同,Repeat会直接复用子组件并对RepeatItem.index索引做对应的更新。
49
50当Repeat将所有重复的键值对比完并做了相应的复用后,若上次的键值有不重复的且本次更新之后有新的键值生成需要新建子组件时,Repeat会复用上次多余的子组件并更新RepeatItem.item数据源和RepeatItem.index索引并刷新UI。
51
52若上次的剩余>=本次新更新的数量,则组件完全复用并释放多余的未被复用的组件。若上次的剩余小于本次新更新的数量,将剩余的组件复用完后,Repeat会新建多出来的数据项对应的组件。
53
54### virtualScroll规则
55
56子组件在Repeat首次渲染只生成当前需要的组件,在滑动和数据更新时会缓存下屏的节点,在需要生成新的组件时,对缓存里的组件进行复用。
57
58#### 滑动场景
59
60滑动前节点现状如下图所示
61
62![Repeat-Start](./figures/Repeat-Start.PNG)
63
64当前Repeat组件templateId有a和b两种,templateId a对应的缓存池,其最大缓存值为3,templateId b对应的缓存池,其最大缓存值为4,其父组件默认预加载节点1个。这时,我们将屏幕右滑,Repeat将开始复用缓存池中的节点。
65
66![Repeat-Slide](./figures/Repeat-Slide.PNG)
67
68index=18的数据进入屏幕及父组件预加载的范围内,此时计算出其templateId为b,这时Repeat会从type=b的缓存池中取出一个节点进行复用,更新它的key&index&data,该子节点内部使用了该项数据及索引的其他孙子节点会根据V2状态管理的规则做同步更新。
69
70index=10的节点划出了屏幕及父组件预加载的范围。当UI主线程空闲时,会去检测type=a的缓存池是否还有空间,此时缓存池中有四个节点,超过了额定的3个,Repeat会释放掉最后一个节点。
71
72![Repeat-Slide-Done](./figures/Repeat-Slide-Done.PNG)
73
74#### 数据更新场景
75
76![Repeat-Start](./figures/Repeat-Start.PNG)
77
78此时我们做如下更新操作,删除index=12节点,更新index=13节点的数据,更新index=14节点的templateId为a,更新index=15节点的key。
79
80![Repeat-Update1](./figures/Repeat-Update1.PNG)
81
82此时Repeat会通知父组件重新布局,逐一对比templateId值,若和原节点templateId值相同,则复用该节点,更新key、index和data,若templateId值发生变化,则复用相应的templateId缓存池中的节点,并更新key、index和data。
83
84![Repeat-Update2](./figures/Repeat-Update2.PNG)
85
86上图显示node13节点更新了数据data和index;node14更新了templateId和index,于是从缓存池中取走一个复用;node15由于key值发生变化并且templateId不变,复用自身节点并同步更新key、index、data;node16和node17均只更新index。index=17的节点是新的,从缓存池中复用。
87
88![Repeat-Update-Done](./figures/Repeat-Update-Done.PNG)
89
90## cachedCount规则
91
92首先需要明确List/Grid `.cachedCount`属性方法和Repeat `cachedCount`的区别。这两者都是为了平衡性能和内存,但是其含义是不同的。
93- List/Grid `.cachedCount`:是指在可见范围外预加载的节点,这些节点会位于组件树上,但不是可见范围内,List/Grid等容器组件会额外渲染这些可见范围外的节点,从而达到其性能收益。Repeat会将这些节点视为“可见”的。
94- template  `cachedCount`: 是指Repeat视为“不可见”的节点,这些节点是空闲的,框架会暂时保存,在需要使用的时候更新这些节点,从而实现复用。
95
96## 使用场景
97
98### non-virtualScroll
99
100#### 数据源变化
101
102在Repeat组件进行非首次渲染时,它会依次对比上次的所有键值和本次更新之后的区别。若当前键值和上次的某一项键值相同,Repeat会直接复用子组件并对RepeatItem.index索引做对应的更新。
103
104当Repeat将所有重复的键值对比完并做了相应的复用后,若上次的键值有不重复的且本次更新之后有新的键值生成需要新建子组件时,Repeat会复用上次多余的子组件并更新RepeatItem.item数据源和RepeatItem.index索引。
105
106若上次的剩余>=本次新更新的数量,则组件完全复用,若上次的剩余小于本次新更新的数量,将剩余的组件复用完后,Repeat会新建多出来的数据项对应的组件。
107
108```ts
109@Entry
110@ComponentV2
111struct Parent {
112  @Local simpleList: Array<string> = ['one', 'two', 'three'];
113
114  build() {
115    Row() {
116      Column() {
117        Text('点击修改第3个数组项的值')
118          .fontSize(24)
119          .fontColor(Color.Red)
120          .onClick(() => {
121            this.simpleList[2] = 'new three';
122          })
123
124        Repeat<string>(this.simpleList)
125            .each((obj: RepeatItem<string>)=>{
126              ChildItem({ item: obj.item })
127                .margin({top: 20})
128            })
129            .key((item: string) => item)
130      }
131      .justifyContent(FlexAlign.Center)
132      .width('100%')
133      .height('100%')
134    }
135    .height('100%')
136    .backgroundColor(0xF1F3F5)
137  }
138}
139
140@ComponentV2
141struct ChildItem {
142  @Param @Require item: string;
143
144  build() {
145    Text(this.item)
146      .fontSize(30)
147  }
148}
149```
150
151![ForEach-Non-Initial-Render-Case-Effect](./figures/ForEach-Non-Initial-Render-Case-Effect.gif)
152
153第三个数组项重新渲染时会复用之前的第三项的组件,仅对数据做了刷新。
154
155#### 索引值变化
156
157下方例子当我们交换数组项1和2时,若键值和上次保持一致,Repeat会复用之前的组件,仅对使用了index索引值的组件做数据刷新。
158
159```ts
160@Entry
161@ComponentV2
162struct Parent {
163  @Local simpleList: Array<string> = ['one', 'two', 'three'];
164
165  build() {
166    Row() {
167      Column() {
168        Text('交换数组项1,2')
169          .fontSize(24)
170          .fontColor(Color.Red)
171          .onClick(() => {
172            let temp: string = this.simpleList[2]
173            this.simpleList[2] = this.simpleList[1]
174            this.simpleList[1] = temp
175          })
176          .margin({bottom: 20})
177
178        Repeat<string>(this.simpleList)
179          .each((obj: RepeatItem<string>)=>{
180            Text("index: " + obj.index)
181              .fontSize(30)
182            ChildItem({ item: obj.item })
183              .margin({bottom: 20})
184          })
185          .key((item: string) => item)
186      }
187      .justifyContent(FlexAlign.Center)
188      .width('100%')
189      .height('100%')
190    }
191    .height('100%')
192    .backgroundColor(0xF1F3F5)
193  }
194}
195
196@ComponentV2
197struct ChildItem {
198  @Param @Require item: string;
199
200  build() {
201    Text(this.item)
202      .fontSize(30)
203  }
204}
205```
206
207![Repeat-Non-Initial-Render-Case-Exchange-Effect](./figures/Repeat-Non-Initial-Render-Case-Exchange-Effect.gif)
208
209### virtualScroll
210
211本小节将展示virtualScroll场景下,Repeat的实际使用场景和组件节点的复用情况。根据复用规则可以衍生出大量的测试场景,篇幅原因,只对典型的数据变化进行解释。
212
213#### 应用示例
214
215下面的代码设计了Repeat组件的virtualScroll场景典型数据源操作,包括**插入数据、修改数据、删除数据、交换数据**。点击相应的文字可以触发数据的变化,依次点击数据项可以交换被点击的两个数据项。
216
217```ts
218@ObservedV2
219class Clazz {
220  @Trace message: string = '';
221
222  constructor(message: string) {
223    this.message = message;
224  }
225}
226
227@Entry
228@ComponentV2
229struct TestPage {
230  @Local simpleList: Array<Clazz> = [];
231  private exchange: number[] = [];
232  private counter: number = 0;
233
234  aboutToAppear(): void {
235    for (let i = 0; i < 100; i++) {
236      this.simpleList.push(new Clazz('Hello ' + i));
237    }
238  }
239
240  build() {
241    Column({ space: 10 }) {
242      Text('点击插入第5项')
243        .fontSize(24)
244        .fontColor(Color.Red)
245        .onClick(() => {
246          this.simpleList.splice(4, 0, new Clazz(`${this.counter++}_new item`));
247        })
248      Text('点击修改第5项')
249        .fontSize(24)
250        .fontColor(Color.Red)
251        .onClick(() => {
252          this.simpleList[4].message = `${this.counter++}_new item`;
253        })
254      Text('点击删除第5项')
255        .fontSize(24)
256        .fontColor(Color.Red)
257        .onClick(() => {
258          this.simpleList.splice(4, 1);
259        })
260      Text('依次点击两个数据项进行交换')
261        .fontSize(24)
262        .fontColor(Color.Red)
263
264      List({ initialIndex: 10 }) {
265        Repeat<Clazz>(this.simpleList)
266          .each((obj: RepeatItem<Clazz>) => {
267            ListItem() {
268              Text('[each] ' + obj.item.message)
269                .fontSize(30)
270                .margin({ top: 10 })
271            }
272          })
273          .key((item: Clazz, index: number) => {
274            return item.message;
275          })
276          .virtualScroll({ totalCount: this.simpleList.length })
277          .templateId((item: Clazz, index: number) => "default")
278          .template('default', (ri) => {
279            Text('[template] ' + ri.item.message)
280              .fontSize(30)
281              .margin({ top: 10 })
282              .onClick(() => {
283                this.exchange.push(ri.index);
284                if (this.exchange.length === 2) {
285                  let _a = this.exchange[0];
286                  let _b = this.exchange[1];
287                  // click to exchange
288                  let temp: string = this.simpleList[_a].message;
289                  this.simpleList[_a].message = this.simpleList[_b].message;
290                  this.simpleList[_b].message = temp;
291                  this.exchange = [];
292                }
293              })
294          }, { cachedCount: 3 })
295      }
296      .cachedCount(1)
297      .border({ width: 1 })
298      .width('90%')
299      .height('70%')
300    }
301    .height('100%')
302    .justifyContent(FlexAlign.Center)
303  }
304}
305```
306该应用列表内容为100项自定义类`Clazz`的`message`字符串属性,List组件的cachedCount设为1,template “default”缓存池大小设为3。应用界面如下图所示:
307
308![Repeat-VirtualScroll-Demo](./figures/Repeat-VirtualScroll-Demo.jpg)
309
310#### 节点操作实例
311
312当进行数据源变化操作时,key值改变的节点会被重新创建。如果相对应的template的缓存池中有缓存节点,就会进行节点复用。当key值不变时,组件会直接复用并更新index的值。
313
314**插入数据**
315
316数据操作:
317
318![Repeat-VirtualScroll-InsertData](./figures/Repeat-VirtualScroll-InsertData.gif)
319
320本例做了四次插入数据操作,前两次为屏幕上方插入数据,后两次为当前屏幕插入数据。打印onUpdateNode函数执行情况“[旧节点key值] -> [新节点key值]”,代表“旧节点”复用“新节点”。节点复用情况如下:
321
322```
323// 屏幕上方两次插入
324onUpdateNode [Hello 22] -> [Hello 8]
325onUpdateNode [Hello 21] -> [Hello 7]
326// 当前屏幕两次插入
327onUpdateNode [Hello 11] -> [2_new item]
328onUpdateNode [Hello 10] -> [3_new item]
329```
330
331在屏幕上方插入数据时,会发生节点移动,引起当前屏幕的预加载节点改变,预加载节点发生了复用,即下方出缓存的节点22复用给了上方进入缓存的节点8。在当前屏幕插入数据时,会产生新数据项,新的节点会复用屏幕下方出缓存的预加载节点。本应用中屏幕下方添加数据时不会发生复用。
332
333**修改数据**
334
335数据操作:
336
337![Repeat-VirtualScroll-ModifyData](./figures/Repeat-VirtualScroll-ModifyData.gif)
338
339本例做了四次修改数据操作,前两次为屏幕上方修改数据,后两次为当前屏幕修改数据。打印onUpdateNode函数执行情况“[旧节点key值] -> [新节点key值]”,代表“旧节点”复用“新节点”。节点复用情况如下:
340
341```
342// 当前屏幕两次修改
343onUpdateNode [1_new item] -> [2_new item]
344onUpdateNode [2_new item] -> [3_new item]
345```
346
347由于屏幕上方/下方的数据不存在渲染节点,所以不会发生节点复用。在当前屏幕修改节点时,由于节点templateId值没有改变,所以复用自身节点,节点id不变。
348
349**交换数据**
350
351数据操作:
352
353![Repeat-VirtualScroll-ExchangeData](./figures/Repeat-VirtualScroll-ExchangeData.gif)
354
355本例在当前屏幕做了两次交换数据操作。由于key值未发生改变,直接交换两个节点,没有节点复用。
356
357**删除数据**
358
359数据操作:
360
361![Repeat-VirtualScroll-DeleteData](./figures/Repeat-VirtualScroll-DeleteData.gif)
362
363本例做了五次删除数据操作,前两次为屏幕上方删除数据,后三次为当前屏幕删除数据。打印onUpdateNode函数执行情况“[旧节点key值] -> [新节点key值]”,代表“旧节点”复用“新节点”。节点复用情况如下:
364
365```
366// 屏幕上方两次删除
367onUpdateNode [Hello 9] -> [Hello 23]
368onUpdateNode [Hello 10] -> [Hello 24]
369// 当前屏幕两次删除没有调用onUpdateNode
370// 当前屏幕第三次删除
371onUpdateNode [Hello 6] -> [Hello 17]
372```
373
374在屏幕上方删除数据时,会发生节点移动,引起当前屏幕的预加载节点改变,预加载节点发生了复用,即上方出缓存的节点9复用给了下方进入缓存的节点23。当前屏幕删除数据时,由于List组件的cachedCount预加载属性,前两次删除操作中,进入屏幕的节点已经渲染,不会发生复用,被删除的节点进入对应template的缓存池中。第三次删除时,下方进入预加载缓存的节点17复用了缓存池中的节点6。
375
376#### 使用多个template
377
378```
379@ObservedV2
380class Wrap1 {
381    @Trace message: string = '';
382
383    constructor(message: string) {
384        this.message = message;
385    }
386}
387
388@Entry
389@ComponentV2
390struct Parent {
391    @Local simpleList: Array<Wrap1> = [];
392
393    aboutToAppear(): void {
394        for (let i=0; i<100; i++) {
395            this.simpleList.push(new Wrap1('Hello' + i));
396        }
397    }
398
399    build() {
400        Column() {
401            List() {
402                Repeat<Wrap1>(this.simpleList)
403                	.each((obj: RepeatItem<Wrap1>)=>{
404                    	ListItem() {
405                    		Row() {
406                    			Text('default index ' + obj.index + ': ')
407                            		.fontSize(30)
408                            	Text(obj.item.message)
409                            		.fontSize(30)
410                    		}
411                        }
412                        .margin(20)
413                	})
414                	.template('odd', (obj: RepeatItem<Wrap1>)=>{
415                    	ListItem() {
416                    		Row() {
417                    			Text('odd index ' + obj.index + ': ')
418                            		.fontSize(30)
419                            		.fontColor(Color.Blue)
420                            	Text(obj.item.message)
421                            		.fontSize(30)
422                            		.fontColor(Color.Blue)
423                    		}
424                        }
425                        .margin(20)
426                	})
427                	.template('even', (obj: RepeatItem<Wrap1>)=>{
428                    	ListItem() {
429                    		Row() {
430                    			Text('even index ' + obj.index + ': ')
431                            		.fontSize(30)
432                            		.fontColor(Color.Green)
433                            	Text(obj.item.message)
434                            		.fontSize(30)
435                            		.fontColor(Color.Green)
436                    		}
437                        }
438                        .margin(20)
439                	})
440                	.templateId((item: Wrap1, index: number) => {
441                		return index%2 ? 'odd' : 'even';
442                	})
443                	.key((item: Wrap1, index: number) => {
444                		return item.message;
445                	})
446            }
447            .cachedCount(5)
448            .width('100%')
449            .height('100%')
450        }
451        .height('100%')
452    }
453}
454```
455
456![Repeat-VirtualScroll-DataChange](./figures/Repeat-VirtualScroll-Template.gif)
457
458#### key值相同时界面异常渲染
459
460当开发者在virtualScroll场景中错误使用了重复key值时,会出现界面渲染异常。
461
462```ts
463@Entry
464@ComponentV2
465struct RepeatKey {
466  @Local simpleList: Array<string> = [];
467
468  aboutToAppear(): void {
469    for (let i = 0; i < 200; i++) {
470      this.simpleList.push(`item ${i}`);
471    }
472  }
473
474  build() {
475    Column({ space: 10 }) {
476      List() {
477        Repeat<string>(this.simpleList)
478          .each((obj: RepeatItem<string>) => {
479            ListItem() {
480              Text(obj.item)
481                .fontSize(30)
482            }
483          })
484          .key((item: string, index: number) => {
485            return 'same key'; // 定义相同键值
486          })
487          .virtualScroll({ totalCount: 200 })
488          .templateId((item:string, index: number) => 'default')
489          .template('default', (ri) => {
490            Text(ri.item)
491              .fontSize(30)
492          }, { cachedCount: 2 })
493      }
494      .cachedCount(2)
495      .border({ width: 1 })
496      .width('90%')
497      .height('70%')
498    }
499    .justifyContent(FlexAlign.Center)
500    .width('100%')
501    .height('100%')
502  }
503}
504```
505
506异常效果如下图(第一个数据项`item 0`消失):
507
508<img src="./figures/Repeat-VirtualScroll-Same-Key.jpg" width="300" />
509
510## 常见问题
511
512### 屏幕外的列表数据发生变化时,保证滚动条位置不变
513
514在List组件中声明Repeat组件,实现key值生成逻辑和each逻辑(如下示例代码),点击按钮“insert”,在屏幕显示的第一个元素前面插入一个元素,屏幕出现向下滚动。
515
516```ts
517// 定义一个类,标记为可观察的
518// 类中自定义一个数组,标记为可追踪的
519@ObservedV2
520class ArrayHolder {
521  @Trace arr: Array<number> = [];
522
523  // constructor,用于初始化数组个数
524  constructor(count: number) {
525    for (let i = 0; i < count; i++) {
526      this.arr.push(i);
527    }
528  }
529}
530
531@Entry
532@ComponentV2
533export struct RepeatTemplateSingle {
534  @Local arrayHolder: ArrayHolder = new ArrayHolder(100);
535  @Local totalCount: number = this.arrayHolder.arr.length;
536  scroller: Scroller = new Scroller();
537
538  build() {
539    Column({ space: 5 }) {
540      List({ space: 20, initialIndex: 19, scroller: this.scroller }) {
541        Repeat(this.arrayHolder.arr)
542          .virtualScroll({ totalCount: this.totalCount })
543          .templateId((item, index) => {
544            return 'number';
545          })
546          .template('number', (r) => {
547            ListItem() {
548              Text(r.index! + ":" + r.item + "Reuse");
549            }
550          })
551          .each((r) => {
552            ListItem() {
553              Text(r.index! + ":" + r.item + "eachMessage");
554            }
555          })
556      }
557      .height('30%')
558
559      Button(`insert totalCount ${this.totalCount}`)
560        .height(60)
561        .onClick(() => {
562          // 插入元素,元素位置为屏幕显示的前一个元素
563          this.arrayHolder.arr.splice(18, 0, this.totalCount);
564          this.totalCount = this.arrayHolder.arr.length;
565        })
566    }
567    .width('100%')
568    .margin({ top: 5 })
569  }
570}
571```
572
573运行效果:
574
575![Repeat-case1-Error](./figures/Repeat-Case1-Error.gif)
576
577在一些场景中,我们不希望屏幕外的数据源变化影响屏幕中List列表Scroller停留的位置,可以通过List组件的[onScrollIndex](../ui/arkts-layout-development-create-list.md#响应滚动位置)事件对列表滚动动作进行监听,当列表发生滚动时,获取列表滚动位置。使用Scroller组件的[scrollToIndex](../reference/apis-arkui/arkui-ts/ts-container-scroll.md#scrolltoindex)特性,滑动到指定index位置,实现屏幕外的数据源增加/删除数据时,Scroller停留的位置不变的效果。
578
579示例代码仅对增加数据的情况进行展示。
580
581```ts
582// 定义一个类,标记为可观察的
583// 类中自定义一个数组,标记为可追踪的
584@ObservedV2
585class ArrayHolder {
586  @Trace arr: Array<number> = [];
587
588  // constructor,用于初始化数组个数
589  constructor(count: number) {
590    for (let i = 0; i < count; i++) {
591      this.arr.push(i);
592    }
593  }
594}
595
596@Entry
597@ComponentV2
598export struct RepeatTemplateSingle {
599  @Local arrayHolder: ArrayHolder = new ArrayHolder(100);
600  @Local totalCount: number = this.arrayHolder.arr.length;
601  scroller: Scroller = new Scroller();
602
603  private start: number = 1;
604  private end: number = 1;
605
606  build() {
607    Column({ space: 5 }) {
608      List({ space: 20, initialIndex: 19, scroller: this.scroller }) {
609        Repeat(this.arrayHolder.arr)
610          .virtualScroll({ totalCount: this.totalCount })
611          .templateId((item, index) => {
612            return 'number';
613          })
614          .template('number', (r) => {
615            ListItem() {
616              Text(r.index! + ":" + r.item + "Reuse");
617            }
618          })
619          .each((r) => {
620            ListItem() {
621              Text(r.index! + ":" + r.item + "eachMessage");
622            }
623          })
624      }
625      .onScrollIndex((start, end) => {
626        this.start = start;
627        this.end = end;
628      })
629      .height('30%')
630
631      Button(`insert totalCount ${this.totalCount}`)
632        .height(60)
633        .onClick(() => {
634          // 插入元素,元素位置为屏幕显示的前一个元素
635          this.arrayHolder.arr.splice(18, 0, this.totalCount);
636          let rect = this.scroller.getItemRect(this.start); // 获取子组件的大小位置
637          this.scroller.scrollToIndex(this.start + 1); // 滑动到指定index
638          this.scroller.scrollBy(0, -rect.y); // 滑动指定距离
639          this.totalCount = this.arrayHolder.arr.length;
640        })
641    }
642    .width('100%')
643    .margin({ top: 5 })
644  }
645}
646```
647
648运行效果:
649
650![Repeat-case1-Succ](./figures/Repeat-Case1-Succ.gif)
651
652### totalCount值大于数据源长度
653
654当数据源总长度很大时,会使用懒加载的方式先加载一部分数据,为了使Repeat显示正确的滚动条样式,需要将数据总长度赋值给totalCount,即数据源全部加载完成前,totalCount大于array.length655
656totalCount > array.length时,在父组件容器滚动过程中,应用需要保证列表即将滑动到数据源末尾时请求后续数据,开发者需要对数据请求的错误场景(如网络延迟)进行保护操作,直到数据源全部加载完成,否则列表滑动的过程中会出现滚动效果异常。
657
658上述规范可以通过实现父组件List/Grid的[onScrollIndex](../ui/arkts-layout-development-create-list.md#响应滚动位置)属性的回调函数完成。示例代码如下:
659
660```ts
661@ObservedV2
662class VehicleData {
663  @Trace name: string;
664  @Trace price: number;
665
666  constructor(name: string, price: number) {
667    this.name = name;
668    this.price = price;
669  }
670}
671
672@ObservedV2
673class VehicleDB {
674  public vehicleItems: VehicleData[] = [];
675
676  constructor() {
677    // init data size 20
678    for (let i = 1; i <= 20; i++) {
679      this.vehicleItems.push(new VehicleData(`Vehicle${i}`, i));
680    }
681  }
682}
683
684@Entry
685@ComponentV2
686struct entryCompSucc {
687  @Local vehicleItems: VehicleData[] = new VehicleDB().vehicleItems;
688  @Local listChildrenSize: ChildrenMainSize = new ChildrenMainSize(60);
689  @Local totalCount: number = this.vehicleItems.length;
690  scroller: Scroller = new Scroller();
691
692  build() {
693    Column({ space: 3 }) {
694      List({ scroller: this.scroller }) {
695        Repeat(this.vehicleItems)
696          .virtualScroll({ totalCount: 50 }) // total data size 50
697          .templateId(() => 'default')
698          .template('default', (ri) => {
699            ListItem() {
700              Column() {
701                Text(`${ri.item.name} + ${ri.index}`)
702                  .width('90%')
703                  .height(this.listChildrenSize.childDefaultSize)
704                  .backgroundColor(0xFFA07A)
705                  .textAlign(TextAlign.Center)
706                  .fontSize(20)
707                  .fontWeight(FontWeight.Bold)
708              }
709            }.border({ width: 1 })
710          }, { cachedCount: 5 })
711          .each((ri) => {
712            ListItem() {
713              Text("Wrong: " + `${ri.item.name} + ${ri.index}`)
714                .width('90%')
715                .height(this.listChildrenSize.childDefaultSize)
716                .backgroundColor(0xFFA07A)
717                .textAlign(TextAlign.Center)
718                .fontSize(20)
719                .fontWeight(FontWeight.Bold)
720            }.border({ width: 1 })
721          })
722          .key((item, index) => `${index}:${item}`)
723      }
724      .height('50%')
725      .margin({ top: 20 })
726      .childrenMainSize(this.listChildrenSize)
727      .alignListItem(ListItemAlign.Center)
728      .onScrollIndex((start, end) => {
729        console.log('onScrollIndex', start, end);
730        // lazy data loading
731        if (this.vehicleItems.length < 50) {
732          for (let i = 0; i < 10; i++) {
733            if (this.vehicleItems.length < 50) {
734              this.vehicleItems.push(new VehicleData("Vehicle_loaded", i));
735            }
736          }
737        }
738      })
739    }
740  }
741}
742```
743
744示例代码运行效果:
745
746![Repeat-Case2-Succ](./figures/Repeat-Case2-Succ.gif)