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.item或RepeatItem.index,将会出现UI渲染异常。 28- virtualScroll场景下,自定义了totalCount值。当数据源长度发生改变时,需要手动更新totalCount值,否则会出现列表显示区域渲染异常。 29 30## 键值生成规则 31 32### non-virtualScroll规则 33 34 35 36### virtualScroll规则 37 38和non-virtualScroll的键值生成规则基本一致,但是不会自动处理重复的键值,需要开发者自己保证键值的唯一性。 39 40 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 63 64当前Repeat组件templateId有a和b两种,templateId a对应的缓存池,其最大缓存值为3,templateId b对应的缓存池,其最大缓存值为4,其父组件默认预加载节点1个。这时,我们将屏幕右滑,Repeat将开始复用缓存池中的节点。 65 66 67 68index=18的数据进入屏幕及父组件预加载的范围内,此时计算出其templateId为b,这时Repeat会从type=b的缓存池中取出一个节点进行复用,更新它的key&index&data,该子节点内部使用了该项数据及索引的其他孙子节点会根据V2状态管理的规则做同步更新。 69 70index=10的节点划出了屏幕及父组件预加载的范围。当UI主线程空闲时,会去检测type=a的缓存池是否还有空间,此时缓存池中有四个节点,超过了额定的3个,Repeat会释放掉最后一个节点。 71 72 73 74#### 数据更新场景 75 76 77 78此时我们做如下更新操作,删除index=12节点,更新index=13节点的数据,更新index=14节点的templateId为a,更新index=15节点的key。 79 80 81 82此时Repeat会通知父组件重新布局,逐一对比templateId值,若和原节点templateId值相同,则复用该节点,更新key、index和data,若templateId值发生变化,则复用相应的templateId缓存池中的节点,并更新key、index和data。 83 84 85 86上图显示node13节点更新了数据data和index;node14更新了templateId和index,于是从缓存池中取走一个复用;node15由于key值发生变化并且templateId不变,复用自身节点并同步更新key、index、data;node16和node17均只更新index。index=17的节点是新的,从缓存池中复用。 87 88 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 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 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 309 310#### 节点操作实例 311 312当进行数据源变化操作时,key值改变的节点会被重新创建。如果相对应的template的缓存池中有缓存节点,就会进行节点复用。当key值不变时,组件会直接复用并更新index的值。 313 314**插入数据** 315 316数据操作: 317 318 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 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 354 355本例在当前屏幕做了两次交换数据操作。由于key值未发生改变,直接交换两个节点,没有节点复用。 356 357**删除数据** 358 359数据操作: 360 361 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 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 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 651 652### totalCount值大于数据源长度 653 654当数据源总长度很大时,会使用懒加载的方式先加载一部分数据,为了使Repeat显示正确的滚动条样式,需要将数据总长度赋值给totalCount,即数据源全部加载完成前,totalCount大于array.length。 655 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