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 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 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 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 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 178 179### 数据更新场景 180 181在上一小节的基础上做如下的数组更新操作,删除index=4的节点,修改节点数据`item_7`为`new_7`。 182 183首先,删除index=4的节点后,失效节点加入`aa`缓存池。后面的列表节点前移,新进入有效加载区域的节点`item_11`会复用`bb`缓存池中的空闲节点,其他节点均只更新索引index。如下图所示。 184 185 186 187其次,节点`item_5`前移,索引index更新为4。根据template type的计算规则,节点`item_5`的template type变为`aa`,需要从`aa`缓存池中复用空闲节点,并且将旧节点加入`bb`缓存池。如下图所示。 188 189 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 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 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 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 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 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 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 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.listItem0、app.media.listItem1、app.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 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.gridItem0、app.media.gridItem1、app.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.gridItem0、app.media.gridItem1、app.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 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 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 1050 1051以下图中的数组变化为例,图中的`item_X`表示数据项的键值key。 1052 1053 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 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 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 1243 1244### totalCount值大于数据源长度 1245 1246当数据源总长度很大时,会使用懒加载的方式先加载一部分数据,为了使Repeat显示正确的滚动条样式,需要将数据总长度赋值给totalCount,即数据源全部加载完成前,totalCount大于array.length。 1247 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 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