说明

* Repeat从API version 12开始支持。
* 本文档仅为开发指南。组件接口规范见[Repeat API参数说明](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-rendering-control-repeat)。
* 由于不同设备屏幕宽高不同，本指南内的示例的实际效果和截图有偏差。

## 概述

Repeat基于数组类型数据来进行循环渲染，一般与滚动容器组件配合使用。

Repeat根据容器组件的**显示区域和预加载区域**加载子组件。当容器滑动/数组改变时，Repeat会根据父容器组件的布局过程重新计算显示区域和预加载区域范围，并管理列表子组件节点的创建与销毁。Repeat通过组件节点更新/复用从而优化性能表现，详细描述见[节点更新/复用能力说明](/consumer/cn/doc/harmonyos-guides/arkts-new-rendering-control-repeat#节点更新复用能力说明)。

说明

Repeat与[LazyForEach](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-rendering-control-lazyforeach)组件的区别：

* Repeat直接监听状态变量的变化，而LazyForEach需要开发者实现[IDataSource](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-rendering-control-lazyforeach#idatasource)接口，手动管理子组件内容/索引的修改。
* Repeat还增强了节点复用能力，提高了长列表滑动和数据更新的渲染性能。
* Repeat增加了渲染模板（template）的能力，在同一个数组中，根据开发者自定义的模板类型（template type）渲染不同的子组件。

## 使用限制

* Repeat必须在滚动类容器组件内使用，仅有[List](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-list)、[ListItemGroup](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-listitemgroup)、[Grid](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-grid)、[Swiper](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-swiper)以及[WaterFlow](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-waterflow)组件支持Repeat懒加载场景。

  循环渲染只允许创建一个子组件，子组件应当是允许包含在容器组件中的子组件。例如：Repeat与[List](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-list)组件配合使用时，子组件必须为[ListItem](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-listitem)组件。
* Repeat不支持V1装饰器，混用V1装饰器会导致渲染异常。
* Repeat当前不支持动画效果。
* 滚动容器组件内只能包含一个Repeat。以List为例，不建议同时包含ListItem、ForEach、LazyForEach，不建议同时包含多个Repeat。
* 当Repeat与自定义组件或[@Builder](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-builder)函数混用时，必须将RepeatItem类型整体进行传参，组件才能监听到数据变化。详见[Repeat与@Builder混用](/consumer/cn/doc/harmonyos-guides/arkts-new-rendering-control-repeat#repeat与builder混用)。

注意

Repeat功能依赖数组属性的动态修改。如果数组对象被密封（sealed）或冻结（frozen），将导致Repeat部分功能失效，因为密封操作会禁止对象扩展属性并锁定现有属性的配置。

常见触发场景：

1）可观察数据的转换：使用[makeObserved](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-statemanagement#makeobserved)将普通数组（如[collections.Array](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-arkts-collections-array)）转换为可观察数据时，某些实现会自动密封数组。

2）主动对象保护：显式调用Object.seal()或Object.freeze()防止数组被修改。

## 循环渲染能力说明

Repeat子组件由.each()和.template()属性定义，只允许包含一个子组件。当页面首次渲染时，Repeat根据当前的容器组件显示区域和预加载区域范围，按需创建子组件。如下图所示：

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/6d/v3/kQD9l6riRnyykiHL7-tK1w/zh-cn_image_0000002540770964.png?HW-CC-KV=V1&HW-CC-Date=20260414T034738Z&HW-CC-Expire=86400&HW-CC-Sign=02E74479581E7A5D9B45F4AB1EB8D30773CC3A2C6BBE98CFEEA6424DFCF29119)

.each()适用于只需要循环渲染一种子组件的场景。下列示例代码使用Repeat组件进行简单的循环渲染。

收起

自动换行

深色代码主题

复制

```
1. // 在List容器组件中使用Repeat
2. @Entry
3. @ComponentV2
4. // 推荐使用V2装饰器
5. struct RepeatExample {
6. @Local dataArr: Array<string> = []; // 数据源

8. aboutToAppear(): void {
9. for (let i = 0; i < 50; i++) {
10. this.dataArr.push(`data_${i}`); // 为数组添加一些数据
11. }
12. }

14. build() {
15. Column() {
16. List() {
17. Repeat<string>(this.dataArr)
18. .each((ri: RepeatItem<string>) => {
19. ListItem() {
20. Text('each_' + ri.item).fontSize(30)
21. }
22. })
23. .virtualScroll({ totalCount: this.dataArr.length }) // 打开懒加载，totalCount为期望加载的数据长度
24. }
25. .cachedCount(2) // 容器组件的预加载区域大小
26. .height('70%')
27. .border({ width: 1 }) // 边框
28. }
29. }
30. }
```

[RepeatExample.ets](https://gitcode.com/HarmonyOS_Samples/guide-snippets/blob/HarmonyOS-feature-20251117/ArkUISample/RenderingControl/entry/src/main/ets/pages/RenderingRepeat/RepeatExample.ets#L16-L46)

运行后界面如下图所示：

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/de/v3/PJ4RA536QE-ETNTVGLXXOQ/zh-cn_image_0000002571291261.png?HW-CC-KV=V1&HW-CC-Date=20260414T034738Z&HW-CC-Expire=86400&HW-CC-Sign=2828D2D6676294153392E76EBD4AA939B8A48FAE7911D20D7031E6C718A29E66)

Repeat提供渲染模板（template）能力，可以在同一个数据源中渲染多种子组件。每个数据项会根据.templateId()得到template type，从而渲染type对应的.template()中的子组件。

* .each()等价于template type为空字符串的.template()。
* 当多个template type相同时（包括template type为空字符串），Repeat仅生效最新定义的.each()或.template()。
* 如果.templateId()缺省，或templateId()计算得到的template type不存在，则template type取默认值空字符串。
* 只有相同template type的节点可以互相复用。

下列示例代码中使用Repeat组件进行循环渲染，并使用了多个渲染模板。

收起

自动换行

深色代码主题

复制

```
1. // 在List容器组件中使用Repeat
2. @Entry
3. @ComponentV2 // 推荐使用V2装饰器
4. struct RepeatExampleWithTemplates {
5. @Local dataArr: Array<string> = []; // 数据源

7. aboutToAppear(): void {
8. for (let i = 0; i < 50; i++) {
9. this.dataArr.push(`data_${i}`); // 为数组添加一些数据
10. }
11. }

13. build() {
14. Column() {
15. List() {
16. Repeat<string>(this.dataArr)
17. .each((ri: RepeatItem<string>) => { // 默认渲染模板
18. ListItem() {
19. Text('each_' + ri.item).fontSize(30).fontColor('rgb(161,10,33)') // 文本颜色为红色
20. }
21. })
22. .key((item: string, index: number): string => JSON.stringify(item)) // 键值生成函数
23. .virtualScroll({ totalCount: this.dataArr.length }) // 打开懒加载，totalCount为期望加载的数据长度
24. .templateId((item: string, index: number): string => { // 根据返回值寻找对应的模板子组件进行渲染
25. return index <= 4 ? 'A' : (index <= 10 ? 'B' : ''); // 前5个节点模板为A，接下来的5个为B，其余为默认模板
26. })
27. .template('A', (ri: RepeatItem<string>) => { // 'A'模板
28. ListItem() {
29. Text('A_' + ri.item).fontSize(30).fontColor('rgb(23,169,141)') // 文本颜色为绿色
30. }
31. }, { cachedCount: 3 }) // 'A'模板的缓存列表容量为3
32. .template('B', (ri: RepeatItem<string>) => { // 'B'模板
33. ListItem() {
34. Text('B_' + ri.item).fontSize(30).fontColor('rgb(39,135,217)') // 文本颜色为蓝色
35. }
36. }, { cachedCount: 4 }) // 'B'模板的缓存列表容量为4
37. }
38. .cachedCount(2) // 容器组件的预加载区域大小
39. .height('70%')
40. .border({ width: 1 }) // 边框
41. }
42. }
43. }
```

[RepeatExample2.ets](https://gitcode.com/HarmonyOS_Samples/guide-snippets/blob/HarmonyOS-feature-20251117/ArkUISample/RenderingControl/entry/src/main/ets/pages/RenderingRepeat/RepeatExample2.ets#L16-L60)

运行后界面如下图所示：

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/85/v3/yYZaIqe8Slau9OFmrJEi4Q/zh-cn_image_0000002540611314.png?HW-CC-KV=V1&HW-CC-Date=20260414T034738Z&HW-CC-Expire=86400&HW-CC-Sign=1162C3F2AE62EADC621B04888C3EF2755C59DC9505C2CC98E62B97E5915A54B9)

## 节点更新/复用能力说明

说明

Repeat子组件的节点操作分为四种：节点创建、节点更新、节点复用、节点销毁。其中，节点更新和节点复用的区别为：

* 节点更新：节点不销毁，状态变量驱动节点属性更新。
* 节点复用：旧节点不销毁，存储在空闲节点缓存池；需要创建新节点时，直接从缓存池中获取可复用的旧节点，并做相应的节点属性更新。

当**滚动容器组件滑动/数组改变**时，Repeat将失效的子组件节点（离开容器组件的显示区域和预加载区域）加入空闲节点缓存池中，即断开组件节点与页面组件树的连接但不销毁节点。在需要生成新的组件时，对缓存池里的组件节点进行复用。

Repeat组件默认开启节点复用功能。从API version 18开始，可以通过配置reusable字段选择是否启用复用功能。为了提高渲染性能，建议开发者保持节点复用。代码示例见[VirtualScrollOptions](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-rendering-control-repeat#virtualscrolloptions)。

从API version 18开始，Repeat支持懒加载模式下[缓存池自定义组件冻结](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-custom-components-freezev2#repeat)。

下面通过典型的[滑动场景](/consumer/cn/doc/harmonyos-guides/arkts-new-rendering-control-repeat#滑动场景)和[数据更新场景](/consumer/cn/doc/harmonyos-guides/arkts-new-rendering-control-repeat#数据更新场景)示例来展示Repeat子组件的渲染逻辑。

定义长度为20的数组，数组前5项的template type为aa，渲染浅蓝色组件，其余项为bb，渲染橙色组件。aa缓存池容量为3，bb缓存池容量为4。容器组件的预加载区域大小为2。为了便于理解，在aa和bb缓存池中分别加入一个和两个空闲节点。

首次渲染，列表的节点状态如下图所示（template type在图中简写为ttype）。

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/30/v3/jjNFY84wR7eep7VpFIJoow/zh-cn_image_0000002571171309.png?HW-CC-KV=V1&HW-CC-Date=20260414T034738Z&HW-CC-Expire=86400&HW-CC-Sign=81DB49737C2C28871E3C0A20AAB138327FB3338D8F90B8135AF0F562E858849C)

### 滑动场景

将屏幕向下滑动一个节点的距离，Repeat会复用缓存池中的节点。

1）index=10的节点进入预加载区域，计算出其template type为bb。由于bb缓存池非空，Repeat会从bb缓存池中取出一个空闲节点进行复用，更新其节点属性（数据item和索引index），该子组件中涉及数据item和索引index的其他孙子组件会根据状态管理V2的规则做同步更新。

2）index=0的节点滑出了预加载区域。当UI主线程空闲时，会检查aa缓存池是否已满，此时aa缓存池未满，将该节点加入到对应的缓存池中。

3）其余节点仍在容器显示区域和预加载区域范围，均只更新索引index。如果对应template type的缓存池已满，Repeat会在UI主线程空闲时销毁掉多余的节点。

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/10/v3/d36YPgUnSeeX6t9Cm3K4Ug/zh-cn_image_0000002540770966.png?HW-CC-KV=V1&HW-CC-Date=20260414T034738Z&HW-CC-Expire=86400&HW-CC-Sign=1AB14AB0B747B4A02657019CF205FDE1EC8C47287158781FBEEC54EA5EDF1544)

### 数据更新场景

在上一小节的基础上做如下的数组更新操作，删除index=4的节点，修改节点数据07为new。

1）删除index=4的节点后，节点05前移。根据template type的计算规则，新的05节点的template type变为aa，直接复用旧的04节点，更新数据item和索引index，并且将旧的05节点加入bb缓存池。

2）后面的列表节点前移，新进入预加载区域的节点11会复用bb缓存池中的空闲节点，其他节点均只更新索引index。

3）对于节点数据从07变为new的情况，页面监听到数据源变化将会触发重新渲染。Repeat数据更新触发重新渲染的逻辑是比较当前索引处节点数据item是否变化，以此判断是否进行UI刷新，仅改变键值不改变item的情况不会触发刷新。

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/27/v3/MxolrJvkRvidydUdv4JgSQ/zh-cn_image_0000002571291263.png?HW-CC-KV=V1&HW-CC-Date=20260414T034738Z&HW-CC-Expire=86400&HW-CC-Sign=AC7234AA4BE3739DB2AC096B29FB7E3FE214288300936B5257B8C129C579F4E6)

### 节点复用情况查看

查看节点是否为复用可以使用[DevEco Testing](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/deveco-testing)工具进行查看，进入DevEco Testing工具后，选择实用工具，界面如下：

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/61/v3/mGLTf2U6Tn-exjBKZQzRKA/zh-cn_image_0000002540611316.png?HW-CC-KV=V1&HW-CC-Date=20260414T034738Z&HW-CC-Expire=86400&HW-CC-Sign=AC8DA6F12C938F04C869FD81CBE51D0F6D68818BA835F36D67CB8A461172BFB0)

在实用工具中选择UIViewer，该工具可以获取设备快照、控件树信息及控件节点属性，在右侧的控件树中选择Repeat子节点，右下方的节点属性会显示节点ID等信息，可以通过节点ID是否相同，判断组件复用或者新建的情况。

## 键值生成函数

Repeat的.key()属性为每个子组件生成一个键值。Repeat通过键值识别数组增加、删除哪些数据以及哪些数据改变了位置（索引）。

注意

键值（key）与索引（index）的区别：键值是数据项的唯一标识符，Repeat根据键值是否发生变化判断数据项是否更新；索引只标识数据项在数组中的位置。

当.key()缺省时，Repeat会生成新的随机键值。当发现有重复key时，Repeat会在已有键值的基础上递归生成新的键值，直到没有重复键值。

键值生成函数.key()的使用限制：

* 即使数组发生变化，开发者也必须保证键值key唯一。
* 每次执行.key()函数时，使用相同的数据项作为输入，输出必须是一致的。

为了实现性能最优，建议开发者自定义键值时，键值的生成应与index无关。因为当前item的键值发生变化后，该item就会被销毁，并重新创建新的item来显示当前view。如果定义的键值与index相关，那么与当前item无关的变更（如前面的数据项增加或删除）可能会触发item的销毁和节点创建，造成不必要的刷新。

* 允许在.key()中使用index，但不建议开发者这样做。因为在数据项移动时索引index发生变化的同时key值也会改变，导致Repeat认为数据发生变化，从而触发子组件重新渲染，降低性能表现。
* 推荐将简单类型数组转换为类对象数组，并添加一个readonly id属性，在构造函数中初始化唯一值。

键值生成示例：

收起

自动换行

深色代码主题

复制

```
1. @ObservedV2
2. class ExampleData {
3. @Trace str: string;
4. num: number;

6. constructor(s: string, n: number) {
7. this.str = s;
8. this.num = n;
9. }
10. }

12. @Entry
13. @ComponentV2
14. struct Index {
15. @Local exampleList: Array<ExampleData> = [];

17. aboutToAppear(): void {
18. for (let i = 0; i < 20; i++) {
19. this.exampleList.push(new ExampleData(`data${i}`, i));
20. }
21. }

23. build() {
24. Column() {
25. List({ space: 10 }) {
26. Repeat(this.exampleList)
27. .each((obj: RepeatItem<ExampleData>) => {
28. ListItem() {
29. Text(obj.item.str).fontSize(50)
30. }
31. })
32. .key(item => item.str) // UI显示刷新与属性str相关，建议在键值生成函数中设置其为返回值，此处键值生成与index无关
33. }
34. }
35. }
36. }
```

在上述示例代码中，使用.key()定义键值生成函数，各子组件的键值为item元素的str属性值。

## 数据精准懒加载

当数据源总长度较长，或数据项加载耗时较长时，可使用Repeat数据精准懒加载特性，避免在初始化时加载所有数据。Repeat数据精准懒加载特性从API version 19开始支持。

开发者可以设置.virtualScroll()的totalCount属性值或onTotalCount自定义方法用于计算期望的数据源长度，设置onLazyLoading属性实现数据精准懒加载，实现在节点首次渲染时加载对应的数据。详细说明和注意事项见[VirtualScrollOptions](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-rendering-control-repeat#virtualscrolloptions)。

**示例1**

数据源总长度较长，在首次渲染、滑动屏幕、跳转显示区域时，动态加载对应区域内的数据。

收起

自动换行

深色代码主题

复制

```
1. @Entry
2. @ComponentV2
3. struct RepeatLazyLoadingLongData {
4. // 假设数据源总长度较长，为1000。初始数组未提供数据。
5. @Local arr: Array<string> = [];
6. scroller: Scroller = new Scroller();

8. build() {
9. Column({ space: 5 }) {
10. // 初始显示位置为index = 100，数据可通过懒加载自动获取。
11. List({ scroller: this.scroller, space: 5, initialIndex: 100 }) {
12. Repeat(this.arr)
13. .virtualScroll({
14. // 期望的数据源总长度为1000。
15. onTotalCount: () => {
16. return 1000;
17. },
18. // 实现数据懒加载。
19. onLazyLoading: (index: number) => {
20. this.arr[index] = index.toString();
21. }
22. })
23. .each((obj: RepeatItem<string>) => {
24. ListItem() {
25. Row({ space: 5 }) {
26. Text(`${obj.index}: Item_${obj.item}`)
27. }
28. }
29. .height(50)
30. })
31. }
32. .height('80%')
33. .border({ width: 1 })

35. // 显示位置跳转至index = 500，数据可通过懒加载自动获取。
36. Button('ScrollToIndex 500')
37. .onClick(() => {
38. this.scroller.scrollToIndex(500);
39. })
40. }
41. }
42. }
```

[RepeatLazyLoading1.ets](https://gitcode.com/HarmonyOS_Samples/guide-snippets/blob/HarmonyOS-feature-20251117/ArkUISample/RenderingControl/entry/src/main/ets/pages/RenderingRepeat/RepeatLazyLoading1.ets#L16-L51)

运行效果：

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/ea/v3/I0Ql9eKUQSGWq5LVV9mEPg/zh-cn_image_0000002571171311.gif?HW-CC-KV=V1&HW-CC-Date=20260414T034738Z&HW-CC-Expire=86400&HW-CC-Sign=22ACC107245EDEBE5121B29FF94D90332D45D37B42146F050BBE7D1F983E911E)

**示例2**

数据加载耗时长，在onLazyLoading方法中，首先为数据项创建占位符，再通过异步任务加载数据。

收起

自动换行

深色代码主题

复制

```
1. @Entry
2. @ComponentV2
3. struct RepeatLazyLoadingSync {
4. @Local arr: Array<string> = [];

6. build() {
7. Column({ space: 5 }) {
8. List({ space: 5 }) {
9. Repeat(this.arr)
10. .virtualScroll({
11. onTotalCount: () => {
12. return 100;
13. },
14. // 实现数据懒加载。
15. onLazyLoading: (index: number) => {
16. // 创建占位符。
17. this.arr[index] = '';
18. // 模拟高耗时加载过程，通过异步任务加载数据。
19. setTimeout(() => {
20. this.arr[index] = index.toString();
21. }, 1000);
22. }
23. })
24. .each((obj: RepeatItem<string>) => {
25. ListItem() {
26. Row({ space: 5 }) {
27. Text(`${obj.index}: Item_${obj.item}`)
28. }
29. }
30. .height(50)
31. })
32. }
33. .height('100%')
34. .border({ width: 1 })
35. }
36. }
37. }
```

[RepeatLazyLoading2.ets](https://gitcode.com/HarmonyOS_Samples/guide-snippets/blob/HarmonyOS-feature-20251117/ArkUISample/RenderingControl/entry/src/main/ets/pages/RenderingRepeat/RepeatLazyLoading2.ets#L16-L49)

运行效果：

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/83/v3/kj8C1HEkR9eSX3ALh9h6_A/zh-cn_image_0000002540770968.gif?HW-CC-KV=V1&HW-CC-Date=20260414T034738Z&HW-CC-Expire=86400&HW-CC-Sign=1ADF42C735B671D35404012CDD4A9458BDD405317450342BBA4C7E124AD91362)

**示例3**

使用数据懒加载，并配合设置onTotalCount: () => { return this.arr.length + 1; }，可实现数据无限懒加载。

注意

* 此场景下，开发者需要提供首屏显示所需的初始数据，并建议设置父容器组件cachedCount > 0，否则将会导致渲染异常。
* 若与Swiper-Loop模式同时使用，停留在index = 0处时，将导致onLazyLoading方法被持续触发，建议避免与Swiper-Loop模式同时使用。
* 开发者需要关注内存消耗情况，避免因数据持续加载而导致内存过量消耗。

收起

自动换行

深色代码主题

复制

```
1. @Entry
2. @ComponentV2
3. struct RepeatLazyLoadingInfinite {
4. @Local arr: Array<string> = [];

6. // 提供首屏显示所需的初始数据。
7. aboutToAppear(): void {
8. for (let i = 0; i < 15; i++) {
9. this.arr.push(i.toString());
10. }
11. }

13. build() {
14. Column({ space: 5 }) {
15. List({ space: 5 }) {
16. Repeat(this.arr)
17. .virtualScroll({
18. // 数据无限懒加载。
19. onTotalCount: () => {
20. return this.arr.length + 1;
21. },
22. onLazyLoading: (index: number) => {
23. this.arr[index] = index.toString();
24. }
25. })
26. .each((obj: RepeatItem<string>) => {
27. ListItem() {
28. Row({ space: 5 }) {
29. Text(`${obj.index}: Item_${obj.item}`)
30. }
31. }
32. .height(50)
33. })
34. }
35. .height('100%')
36. .border({ width: 1 })
37. // 建议设置cachedCount > 0。
38. .cachedCount(1)
39. }
40. }
41. }
```

[RepeatLazyLoading3.ets](https://gitcode.com/HarmonyOS_Samples/guide-snippets/blob/HarmonyOS-feature-20251117/ArkUISample/RenderingControl/entry/src/main/ets/pages/RenderingRepeat/RepeatLazyLoading3.ets#L16-L52)

运行效果：

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/b2/v3/2YIEtafbRMyvNpaIaxJLlw/zh-cn_image_0000002571291265.gif?HW-CC-KV=V1&HW-CC-Date=20260414T034738Z&HW-CC-Expire=86400&HW-CC-Sign=FA72C2298E8CA0F4B3257DF72278EC383E62E20A1048AFEB07F765E79C7A4376)

## 拖拽排序

当Repeat在List组件下使用，并且设置了[onMove](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-universal-attributes-drag-sorting#onmove)事件，Repeat每次迭代都生成一个ListItem时，可以使能拖拽排序。Repeat拖拽排序特性从API version 19开始支持。

注意

* 拖拽排序离手后，如果数据位置发生变化，则会触发onMove事件，上报数据移动原始索引号和目标索引号。

  在onMove事件中，需要根据上报的起始索引号和目标索引号修改数据源。数据源修改前后，要保持每个数据的键值不变，只是顺序发生变化，才能保证落位动画正常执行。
* 拖拽排序过程中，在离手之前，不允许修改数据源。

示例代码：

收起

自动换行

深色代码主题

复制

```
1. @Entry
2. @ComponentV2
3. struct RepeatVirtualScrollOnMove {
4. @Local simpleList: Array<string> = [];

6. aboutToAppear(): void {
7. for (let i = 0; i < 100; i++) {
8. this.simpleList.push(`${i}`);
9. }
10. }

12. build() {
13. Column() {
14. List() {
15. Repeat<string>(this.simpleList)
16. // 通过设置onMove，使能拖拽排序。
17. .onMove((from: number, to: number) => {
18. let temp = this.simpleList.splice(from, 1);
19. this.simpleList.splice(to, 0, temp[0]);
20. })
21. .each((obj: RepeatItem<string>) => {
22. ListItem() {
23. Text(obj.item)
24. .fontSize(16)
25. .textAlign(TextAlign.Center)
26. .size({ height: 100, width: '100%' })
27. }.margin(10)
28. .borderRadius(10)
29. .backgroundColor('#FFFFFFFF')
30. })
31. .key((item: string, index: number) => {
32. return item;
33. })
34. .virtualScroll({ totalCount: this.simpleList.length })
35. }
36. .border({ width: 1 })
37. .backgroundColor('#FFDCDCDC')
38. .width('100%')
39. .height('100%')
40. }
41. }
42. }
```

[RepeatVirtualScrollOnMove.ets](https://gitcode.com/HarmonyOS_Samples/guide-snippets/blob/HarmonyOS-feature-20251117/ArkUISample/RenderingControl/entry/src/main/ets/pages/RenderingRepeat/RepeatVirtualScrollOnMove.ets#L16-L59)

运行效果：

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/3f/v3/4U-4O4-wSZi1mdeVWml5Ew/zh-cn_image_0000002540611318.gif?HW-CC-KV=V1&HW-CC-Date=20260414T034738Z&HW-CC-Expire=86400&HW-CC-Sign=52993BA3E8E33FD1E6D4809B65A5BC3C483CC59EAE1F8EBC687EF8E8B91E8BE6)

## 前插保持

前插保持，即在显示区域之前插入或删除数据后，保持显示区域的子组件位置不变。

从API version 20开始，仅当父容器组件为List且[maintainVisibleContentPosition](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-list#maintainvisiblecontentposition12)属性设置为true后，在List显示区域之前插入或删除数据时保持List显示区域子组件位置不变。

**示例代码**

收起

自动换行

深色代码主题

复制

```
1. @Entry
2. @ComponentV2
3. struct PreInsertDemo {
4. @Local simpleList: Array<string> = [];
5. private cnt: number = 1;

7. aboutToAppear(): void {
8. for (let i = 0; i < 30; i++) {
9. this.simpleList.push(`Hello ${this.cnt++}`);
10. }
11. }

13. build() {
14. Column() {
15. Row() {
16. Button(`insert #5`)
17. .onClick(() => {
18. this.simpleList.splice(5, 0, `Hello ${this.cnt++}`);
19. })
20. Button(`delete #0`)
21. .onClick(() => {
22. this.simpleList.splice(0, 1);
23. })
24. }

26. List({ initialIndex: 5 }) {
27. Repeat<string>(this.simpleList)
28. .each((obj: RepeatItem<string>) => {
29. ListItem() {
30. Row() {
31. Text(`index: ${obj.index}  `)
32. .fontSize(16)
33. .fontColor('#70707070')
34. .textAlign(TextAlign.End)
35. .size({ height: 100, width: '40%' })
36. Text(`item: ${obj.item}`)
37. .fontSize(16)
38. .textAlign(TextAlign.Start)
39. .size({ height: 100, width: '60%' })
40. }
41. }.margin(10)
42. .borderRadius(10)
43. .backgroundColor('#FFFFFFFF')
44. })
45. .key((item: string, index: number) => item)
46. .virtualScroll({ totalCount: this.simpleList.length })
47. }
48. .maintainVisibleContentPosition(true) // 启用前插保持
49. .border({ width: 1 })
50. .backgroundColor('#FFDCDCDC')
51. .width('100%')
52. .height('100%')
53. }
54. }
55. }
```

[PreInsert.ets](https://gitcode.com/HarmonyOS_Samples/guide-snippets/blob/HarmonyOS-feature-20251117/ArkUISample/RenderingControl/entry/src/main/ets/pages/RenderingRepeat/PreInsert.ets#L16-L72)

示例中，通过点击按钮在显示区域上方插入或删除数据时，显示区域的节点仅index发生改变，对应数据项不变。

运行效果：

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/73/v3/T7BnUh1pTX27pd-fI-mBdg/zh-cn_image_0000002571171313.gif?HW-CC-KV=V1&HW-CC-Date=20260414T034738Z&HW-CC-Expire=86400&HW-CC-Sign=3DD2A8D1EFF60FA8DAB770102458F428E74B78451039535744F995CED8ED9665)

## 常见使用场景

### 数据展示&操作

下面的代码示例展示了Repeat修改数组的常见操作，包括**插入数据、修改数据、删除数据、交换数据**。点击下拉框选择索引index值，点击相应的按钮即可操作数据项，依次点击两个不同的数据项可以进行交换。

收起

自动换行

深色代码主题

复制

```
1. @ObservedV2
2. class Repeat006Clazz {
3. @Trace public message: string = '';

5. constructor(message: string) {
6. this.message = message;
7. }
8. }

10. @Entry
11. @ComponentV2
12. struct RepeatVirtualScroll {
13. @Local simpleList: Array<Repeat006Clazz> = [];
14. private exchange: number[] = [];
15. private counter: number = 0;
16. @Local selectOptions: SelectOption[] = [];
17. @Local selectIdx: number = 0;

19. @Monitor('simpleList')
20. reloadSelectOptions(): void {
21. this.selectOptions = [];
22. for (let i = 0; i < this.simpleList.length; ++i) {
23. this.selectOptions.push({ value: i.toString() });
24. }
25. if (this.selectIdx >= this.simpleList.length) {
26. this.selectIdx = this.simpleList.length - 1;
27. }
28. }

30. aboutToAppear(): void {
31. for (let i = 0; i < 100; i++) {
32. this.simpleList.push(new Repeat006Clazz(`item_${i}`));
33. }
34. this.reloadSelectOptions();
35. }

37. handleExchange(idx: number): void { // 点击交换子组件
38. this.exchange.push(idx);
39. if (this.exchange.length === 2) {
40. let _a = this.exchange[0];
41. let _b = this.exchange[1];
42. let temp: Repeat006Clazz = this.simpleList[_a];
43. this.simpleList[_a] = this.simpleList[_b];
44. this.simpleList[_b] = temp;
45. this.exchange = [];
46. }
47. }

49. build() {
50. Column({ space: 10 }) {
51. Text('virtualScroll each()&template() 2t')
52. .fontSize(15)
53. .fontColor(Color.Gray)
54. Text('Select an index and press the button to update data.')
55. .fontSize(15)
56. .fontColor(Color.Gray)

58. Select(this.selectOptions)
59. .selected(this.selectIdx)
60. .value(this.selectIdx.toString())
61. .key('selectIdx')
62. .onSelect((index: number) => {
63. this.selectIdx = index;
64. })
65. Row({ space: 5 }) {
66. Button('Add No.' + this.selectIdx)
67. .onClick(() => {
68. this.simpleList.splice(this.selectIdx, 0, new Repeat006Clazz(`${this.counter++}_add_item`));
69. this.reloadSelectOptions();
70. })
71. Button('Modify No.' + this.selectIdx)
72. .onClick(() => {
73. this.simpleList.splice(this.selectIdx, 1, new Repeat006Clazz(`${this.counter++}_modify_item`));
74. })
75. Button('Del No.' + this.selectIdx)
76. .onClick(() => {
77. this.simpleList.splice(this.selectIdx, 1);
78. this.reloadSelectOptions();
79. })
80. }
81. Button('Update array length to 5')
82. .onClick(() => {
83. this.simpleList = this.simpleList.slice(0, 5);
84. this.reloadSelectOptions();
85. })

87. Text('Click on two items to exchange')
88. .fontSize(15)
89. .fontColor(Color.Gray)

91. List({ space: 10 }) {
92. Repeat<Repeat006Clazz>(this.simpleList)
93. .each((obj: RepeatItem<Repeat006Clazz>) => {
94. ListItem() {
95. Text(`[each] index${obj.index}: ${obj.item.message}`)
96. .fontSize(25)
97. .onClick(() => {
98. this.handleExchange(obj.index);
99. })
100. }
101. })
102. .key((item: Repeat006Clazz, index: number) => {
103. return item.message;
104. })
105. .virtualScroll({ totalCount: this.simpleList.length })
106. .templateId((item: Repeat006Clazz, index: number) => {
107. return (index % 2 === 0) ? 'odd' : 'even';
108. })
109. .template('odd', (ri) => {
110. Text(`[odd] index${ri.index}: ${ri.item.message}`)
111. .fontSize(25)
112. .fontColor(Color.Blue)
113. .onClick(() => {
114. this.handleExchange(ri.index);
115. })
116. }, { cachedCount: 3 })
117. .template('even', (ri) => {
118. Text(`[even] index${ri.index}: ${ri.item.message}`)
119. .fontSize(25)
120. .fontColor(Color.Green)
121. .onClick(() => {
122. this.handleExchange(ri.index);
123. })
124. }, { cachedCount: 1 })
125. }
126. .cachedCount(2)
127. .border({ width: 1 })
128. .width('95%')
129. .height('40%')
130. }
131. .justifyContent(FlexAlign.Center)
132. .width('100%')
133. .height('100%')
134. }
135. }
```

[RepeatVirtualScroll2T.ets](https://gitcode.com/HarmonyOS_Samples/guide-snippets/blob/HarmonyOS-feature-20251117/ArkUISample/RenderingControl/entry/src/main/ets/pages/RenderingRepeat/RepeatVirtualScroll2T.ets#L16-L152)

该示例代码展示了100项自定义类RepeatClazz的message字符串属性，List组件的cachedCount属性设为2，模板'odd'和'even'的空闲节点缓存池大小分别设为3和1。运行后界面如下图所示：

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/d7/v3/em-kZCYFQz6I_NkrWHnbCg/zh-cn_image_0000002540770970.gif?HW-CC-KV=V1&HW-CC-Date=20260414T034738Z&HW-CC-Expire=86400&HW-CC-Sign=04E50DDE540FE590EDC6D056B03E97D8FA8962529E117B23F6A98D6D367C9D76)

### Repeat嵌套

Repeat支持嵌套使用，示例代码如下：

收起

自动换行

深色代码主题

复制

```
1. // Repeat嵌套
2. @Entry
3. @ComponentV2
4. struct NestedRepeat {
5. @Local outerList: string[] = [];
6. @Local innerList: number[] = [];

8. aboutToAppear(): void {
9. for (let i = 0; i < 20; i++) {
10. this.outerList.push(i.toString());
11. this.innerList.push(i);
12. }
13. }

15. build() {
16. Column({ space: 20 }) {
17. Text('Nested Repeat with virtualScroll')
18. .fontSize(15)
19. .fontColor(Color.Gray)
20. List() {
21. Repeat<string>(this.outerList)
22. .each((obj) => {
23. ListItem() {
24. Column() {
25. Text('outerList item: ' + obj.item)
26. .fontSize(30)
27. List() {
28. Repeat<number>(this.innerList)
29. .each((subObj) => {
30. ListItem() {
31. Text('innerList item: ' + subObj.item)
32. .fontSize(20)
33. }
34. })
35. .key((item) => 'innerList_' + item)
36. .virtualScroll()
37. }
38. .width('80%')
39. .border({ width: 1 })
40. .backgroundColor(Color.Orange)
41. }
42. .height('30%')
43. .backgroundColor(Color.Pink)
44. }
45. .border({ width: 1 })
46. })
47. .key((item) => 'outerList_' + item)
48. .virtualScroll()
49. }
50. .width('80%')
51. .border({ width: 1 })
52. }
53. .justifyContent(FlexAlign.Center)
54. .width('90%')
55. .height('80%')
56. }
57. }
```

[NestedRepeat.ets](https://gitcode.com/HarmonyOS_Samples/guide-snippets/blob/HarmonyOS-feature-20251117/ArkUISample/RenderingControl/entry/src/main/ets/pages/RenderingRepeat/NestedRepeat.ets#L16-L74)

运行效果：

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/ba/v3/UlPaufTvSmisWIQpKdvAkQ/zh-cn_image_0000002571291267.png?HW-CC-KV=V1&HW-CC-Date=20260414T034738Z&HW-CC-Expire=86400&HW-CC-Sign=A0A010123BA05B8CAF4765EE4E51B725CCAB3B42D895D93779F597CCD652BF69)

### 父容器组件应用场景

本节展示Repeat与滚动容器组件的常见应用场景。

**与List组合使用**

在List容器组件中使用Repeat，示例代码如下：

收起

自动换行

深色代码主题

复制

```
1. class DemoListItemInfo {
2. public name: string;
3. public icon: Resource;

5. constructor(name: string, icon: Resource) {
6. this.name = name;
7. this.icon = icon;
8. }
9. }

11. @Entry
12. @ComponentV2
13. struct DemoList {
14. @Local videoList: Array<DemoListItemInfo> = [];

16. aboutToAppear(): void {
17. for (let i = 0; i < 10; i++) {
18. // 此处app.media.listItem0、app.media.listItem1、app.media.listItem2仅作示例，请开发者自行替换
19. this.videoList.push(new DemoListItemInfo('Video' + i,
20. i % 3 == 0 ? $r('app.media.listItem0') :
21. i % 3 == 1 ? $r('app.media.listItem1') : $r('app.media.listItem2')));
22. }
23. }

25. @Builder
26. itemEnd(index: number) {
27. Button('Delete')
28. .backgroundColor(Color.Red)
29. .onClick(() => {
30. this.videoList.splice(index, 1);
31. })
32. }

34. build() {
35. Column({ space: 10 }) {
36. Text('List Contains the Repeat Component')
37. .fontSize(15)
38. .fontColor(Color.Gray)

40. List({ space: 5 }) {
41. Repeat<DemoListItemInfo>(this.videoList)
42. .each((obj: RepeatItem<DemoListItemInfo>) => {
43. ListItem() {
44. Column() {
45. Image(obj.item.icon)
46. .width('80%')
47. .margin(10)
48. Text(obj.item.name)
49. .fontSize(20)
50. }
51. }
52. .swipeAction({
53. end: {
54. builder: () => {
55. this.itemEnd(obj.index);
56. }
57. }
58. })
59. .onAppear(() => {
60. })
61. })
62. .key((item: DemoListItemInfo) => item.name)
63. .virtualScroll()
64. }
65. .cachedCount(2)
66. .height('90%')
67. .border({ width: 1 })
68. .listDirection(Axis.Vertical)
69. .alignListItem(ListItemAlign.Center)
70. .divider({
71. strokeWidth: 1,
72. startMargin: 60,
73. endMargin: 60,
74. color: '#ffe9f0f0'
75. })

77. Row({ space: 10 }) {
78. Button('Delete No.1')
79. .onClick(() => {
80. this.videoList.splice(0, 1);
81. })
82. Button('Delete No.5')
83. .onClick(() => {
84. this.videoList.splice(4, 1);
85. })
86. }
87. }
88. .width('100%')
89. .height('100%')
90. .justifyContent(FlexAlign.Center)
91. }
92. }
```

[DemoList.ets](https://gitcode.com/HarmonyOS_Samples/guide-snippets/blob/HarmonyOS-feature-20251117/ArkUISample/RenderingControl/entry/src/main/ets/pages/RenderingRepeat/DemoList.ets#L16-L109)

右滑并点击按钮，或点击底部按钮，可删除视频卡片：

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/4f/v3/7szpQdhxQruysddK_QMkZg/zh-cn_image_0000002540611320.gif?HW-CC-KV=V1&HW-CC-Date=20260414T034738Z&HW-CC-Expire=86400&HW-CC-Sign=5AB93486310023B25F46A9EDE34DD3B75E182FE40F5513249F404CC5A6B5AE86)

**与Grid组合使用**

在Grid容器组件中使用Repeat，示例如下：

收起

自动换行

深色代码主题

复制

```
1. import { hilog } from '@kit.PerformanceAnalysisKit';
2. const TAG = '[Sample_RenderingControl]';
3. const DOMAIN = 0xF811;

5. class DemoGridItemInfo {
6. public name: string;
7. public icon: Resource;

9. constructor(name: string, icon: Resource) {
10. this.name = name;
11. this.icon = icon;
12. }
13. }

15. @Entry
16. @ComponentV2
17. struct DemoGrid {
18. @Local itemList: Array<DemoGridItemInfo> = [];
19. @Local isRefreshing: boolean = false;
20. private layoutOptions: GridLayoutOptions = {
21. regularSize: [1, 1],
22. irregularIndexes: [10]
23. };
24. private gridScroller: Scroller = new Scroller();
25. private num: number = 0;

27. aboutToAppear(): void {
28. for (let i = 0; i < 10; i++) {
29. // 此处app.media.gridItem0、app.media.gridItem1、app.media.gridItem2仅作示例，请开发者自行替换
30. this.itemList.push(new DemoGridItemInfo('Video' + i,
31. i % 3 == 0 ? $r('app.media.gridItem0') :
32. i % 3 == 1 ? $r('app.media.gridItem1') : $r('app.media.gridItem2')));
33. }
34. }

36. build() {
37. Column({ space: 10 }) {
38. Text('Grid Contains the Repeat Component')
39. .fontSize(15)
40. .fontColor(Color.Gray)

42. Refresh({ refreshing: $$this.isRefreshing }) {
43. Grid(this.gridScroller, this.layoutOptions) {
44. Repeat<DemoGridItemInfo>(this.itemList)
45. .each((obj: RepeatItem<DemoGridItemInfo>) => {
46. if (obj.index === 10 ) {
47. GridItem() {
48. Text('Last viewed here. Touch to refresh.')
49. .fontSize(20)
50. }
51. .height(30)
52. .border({ width: 1 })
53. .onClick(() => {
54. this.gridScroller.scrollToIndex(0);
55. this.isRefreshing = true;
56. })
57. .onAppear(() => {
58. hilog.info(DOMAIN, TAG, 'AceTag', obj.item.name);
59. })
60. } else {
61. GridItem() {
62. Column() {
63. Image(obj.item.icon)
64. .width('100%')
65. .height(80)
66. .objectFit(ImageFit.Cover)
67. .borderRadius({ topLeft: 16, topRight: 16 })
68. Text(obj.item.name)
69. .fontSize(15)
70. .height(20)
71. }
72. }
73. .height(100)
74. .borderRadius(16)
75. .backgroundColor(Color.White)
76. .onAppear(() => {
77. hilog.info(DOMAIN, TAG, 'AceTag', obj.item.name);
78. })
79. }
80. })
81. .key((item: DemoGridItemInfo) => item.name)
82. .virtualScroll()
83. }
84. .columnsTemplate('repeat(auto-fit, 150)')
85. .cachedCount(4)
86. .rowsGap(15)
87. .columnsGap(10)
88. .height('100%')
89. .padding(10)
90. .backgroundColor('#F1F3F5')
91. }
92. .onRefreshing(() => {
93. setTimeout(() => {
94. this.itemList.splice(10, 1);
95. this.itemList.unshift(new DemoGridItemInfo('refresh', $r('app.media.gridItem0'))); // 此处app.media.gridItem0仅作示例，请开发者自行替换
96. for (let i = 0; i < 10; i++) {
97. // 此处app.media.gridItem0、app.media.gridItem1、app.media.gridItem2仅作示例，请开发者自行替换
98. this.itemList.unshift(new DemoGridItemInfo('New video' + this.num,
99. i % 3 == 0 ? $r('app.media.gridItem0') :
100. i % 3 == 1 ? $r('app.media.gridItem1') : $r('app.media.gridItem2')));
101. this.num++;
102. }
103. this.isRefreshing = false;
104. }, 1000);
105. })
106. .refreshOffset(64)
107. .pullToRefresh(true)
108. .width('100%')
109. .height('85%')

111. Button('Refresh')
112. .onClick(() => {
113. this.gridScroller.scrollToIndex(0);
114. this.isRefreshing = true;
115. })
116. }
117. .width('100%')
118. .height('100%')
119. .justifyContent(FlexAlign.Center)
120. }
121. }
```

[DemoGrid.ets](https://gitcode.com/HarmonyOS_Samples/guide-snippets/blob/HarmonyOS-feature-20251117/ArkUISample/RenderingControl/entry/src/main/ets/pages/RenderingRepeat/DemoGrid.ets#L16-L138)

下拉屏幕，或点击刷新按钮，或点击“先前浏览至此，点击刷新”，可加载新的视频内容：

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/ac/v3/LP2WJX1qQSS_xRXANpYHdw/zh-cn_image_0000002571171315.gif?HW-CC-KV=V1&HW-CC-Date=20260414T034738Z&HW-CC-Expire=86400&HW-CC-Sign=A2AEA929B07EEFF20C98101BAB65833F584668712171DF1EFC39A6C40AD8C6FD)

**与Swiper组合使用**

在Swiper容器组件中使用Repeat，示例如下：

收起

自动换行

深色代码主题

复制

```
1. const remotePictures: string[] = [
2. 'common/image/image1.png', // 请填写具体的图片地址
3. 'common/image/image2.png',
4. 'common/image/image3.png',
5. ];

7. @ObservedV2
8. class DemoSwiperItemInfo {
9. public id: string;
10. @Trace public url: string = 'default';

12. constructor(id: string) {
13. this.id = id;
14. }
15. }

17. @Entry
18. @ComponentV2
19. struct DemoSwiper {
20. @Local pics: Array<DemoSwiperItemInfo> = [];

22. aboutToAppear(): void {
23. for (let i = 0; i < 3; i++) {
24. this.pics.push(new DemoSwiperItemInfo('pic' + i));
25. }
26. setTimeout(() => {
27. this.pics[0].url = remotePictures[0];
28. }, 1000);
29. }

31. build() {
32. Column() {
33. Text('Swiper Contains the Repeat Component')
34. .fontSize(15)
35. .fontColor(Color.Gray)

37. Stack() {
38. Text('Loading...')
39. .fontSize(15)
40. .fontColor(Color.Gray)
41. Swiper() {
42. Repeat(this.pics)
43. .each((obj: RepeatItem<DemoSwiperItemInfo>) => {
44. Image(obj.item.url)
45. .onAppear(() => {
46. })
47. })
48. .key((item: DemoSwiperItemInfo) => item.id)
49. .virtualScroll()
50. }
51. .cachedCount(9)
52. .height('50%')
53. .loop(false)
54. .indicator(true)
55. .onChange((index) => {
56. setTimeout(() => {
57. this.pics[index].url = remotePictures[index];
58. }, 1000);
59. })
60. }
61. .width('100%')
62. .height('100%')
63. .backgroundColor(Color.Black)
64. }
65. }
66. }
```

[DemoSwiper.ets](https://gitcode.com/HarmonyOS_Samples/guide-snippets/blob/HarmonyOS-feature-20251117/ArkUISample/RenderingControl/entry/src/main/ets/pages/RenderingRepeat/DemoSwiper.ets#L16-L83)

定时1秒后加载图片，模拟网络延迟：

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/28/v3/bHcBUgSiSiOzL3rwCSSRHg/zh-cn_image_0000002540770972.gif?HW-CC-KV=V1&HW-CC-Date=20260414T034738Z&HW-CC-Expire=86400&HW-CC-Sign=1564D7856156EC98493D48021A77A5C019142FFDEE0679C66893A41E30E96F80)

## 关闭懒加载

当关闭Repeat的.virtualScroll()属性时（即省略该属性），Repeat在初始化页面时加载列表中的所有子组件，适合**短数据列表/组件全部加载**的场景。对于**长数据列表（数据长度大于30）**，如果关闭懒加载，Repeat会一次性加载全量子组件，此操作耗时长，不建议使用。

注意

* 渲染模板特性（template）不可用。
* 不受滚动容器组件的限制，可以在任意场景使用。
* 支持与V1装饰器混用。
* 页面刷新取决于键值变化：如果键值相同，即使数据改变，页面也不会刷新。详见[节点更新能力说明](/consumer/cn/doc/harmonyos-guides/arkts-new-rendering-control-repeat#节点更新能力说明)。

### 节点更新能力说明

（关闭懒加载后）页面首次渲染时，Repeat子组件全部创建。数组发生改变后，Repeat对子组件节点的处理分为以下几个步骤：

首先，遍历旧数组键值。如果新数组中没有该键值，将其加入键值集合deletedKeys。

其次，遍历新数组键值。依次判断以下条件，进行符合条件的操作：

1. 若在旧数组中能找到相同键值，直接使用对应的子组件节点，并更新索引index。
2. 若deletedKeys非空，按照先进后出的顺序，更新该集合中的键值所对应的节点。
3. 若deletedKeys为空，则表示没有可以更新的节点，需要创建新节点。

最后，如果新数组键值遍历结束后，deletedKeys非空，则销毁集合中的键值所对应的节点。

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/30/v3/4XAm_7yWR7iAynKyH9t_Yg/zh-cn_image_0000002571291269.png?HW-CC-KV=V1&HW-CC-Date=20260414T034738Z&HW-CC-Expire=86400&HW-CC-Sign=CBFCABD06BE63953EA60470BAA3AD6E934F4EC5EE6083637173E283657D0BED5)

以下图中的数组变化为例，图中的item\_X表示数据项的键值key。

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/a3/v3/vEiEcOt-SLiAMnVdxD_niA/zh-cn_image_0000002540611322.png?HW-CC-KV=V1&HW-CC-Date=20260414T034738Z&HW-CC-Expire=86400&HW-CC-Sign=A3B91E719B7B13C37DE6949A988CD8CAC4DA474AA4427F2E2370E79E0180771D)

根据上述判断逻辑：item\_0没有变化，item\_1和item\_2只更新了索引，item\_n1和item\_n2分别由item\_4和item\_3进行节点更新获得，item\_n3为新创建的节点。

说明

Repeat关闭懒加载场景与[ForEach](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-rendering-control-foreach)组件的区别：

* 针对特定数组更新场景的渲染性能进行了优化
* 将子组件的内容/索引管理职责转移至框架层面

### 示例

收起

自动换行

深色代码主题

复制

```
1. @Entry
2. @ComponentV2
3. struct NodeUpdateMechanism {
4. @Local simpleList: Array<string> = ['one', 'two', 'three'];

6. build() {
7. Row() {
8. Column() {
9. Text('Click to change the value of the third array item')
10. .fontSize(24)
11. .fontColor(Color.Red)
12. .onClick(() => {
13. this.simpleList[2] = 'new three';
14. })

16. Repeat<string>(this.simpleList)
17. .each((obj: RepeatItem<string>)=>{
18. ChildItem({ item: obj.item })
19. .margin({top: 20})
20. })
21. .key((item: string) => item)
22. }
23. .justifyContent(FlexAlign.Center)
24. .width('100%')
25. .height('100%')
26. }
27. .height('100%')
28. .backgroundColor(0xF1F3F5)
29. }
30. }

32. @ComponentV2
33. struct ChildItem {
34. @Param @Require item: string;

36. build() {
37. Text(this.item)
38. .fontSize(30)
39. }
40. }
```

[NodeUpdateMechanism.ets](https://gitcode.com/HarmonyOS_Samples/guide-snippets/blob/HarmonyOS-feature-20251117/ArkUISample/RenderingControl/entry/src/main/ets/pages/RenderingRepeat/NodeUpdateMechanism.ets#L16-L57)

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/8d/v3/zMx258rtTkS4MLv11zU6RQ/zh-cn_image_0000002571291243.gif?HW-CC-KV=V1&HW-CC-Date=20260414T034738Z&HW-CC-Expire=86400&HW-CC-Sign=14B4A29E8CF70FA8DACB7BB0B3BE5AAA4205550E94F0A11DD97777D8E0526EC3)

点击红色字体，第三个数据项发生变化（直接使用旧的组件节点，仅刷新数据）。

## 常见问题

### 屏幕外的列表数据发生变化时，保证滚动条位置不变

以下示例中，屏幕外的数据源变化将影响屏幕中List列表Scroller停留的位置：

在List组件中声明Repeat组件，实现key值生成逻辑和each逻辑（如下示例代码），点击按钮“insert”，在屏幕显示的第一个元素前面插入一个元素，屏幕出现向下滚动。

收起

自动换行

深色代码主题

复制

```
1. // 定义一个类，标记为可观察的
2. // 类中自定义一个数组，标记为可追踪的
3. @ObservedV2
4. class ArrayHolder {
5. @Trace public arr: Array<number> = [];

7. // constructor，用于初始化数组个数
8. constructor(count: number) {
9. for (let i = 0; i < count; i++) {
10. this.arr.push(i);
11. }
12. }
13. }

15. @Entry
16. @ComponentV2
17. struct RepeatTemplateSingle {
18. @Local arrayHolder: ArrayHolder = new ArrayHolder(100);
19. @Local totalCount: number = this.arrayHolder.arr.length;
20. scroller: Scroller = new Scroller();

22. build() {
23. Column({ space: 5 }) {
24. List({ space: 20, initialIndex: 19, scroller: this.scroller }) {
25. Repeat(this.arrayHolder.arr)
26. .virtualScroll({ totalCount: this.totalCount })
27. .templateId((item, index) => {
28. return 'number';
29. })
30. .template('number', (r) => {
31. ListItem() {
32. Text(r.index! + ':' + r.item + 'Reuse');
33. }
34. })
35. .each((r) => {
36. ListItem() {
37. Text(r.index! + ':' + r.item + 'eachMessage');
38. }
39. })
40. }
41. .height('30%')

43. Button(`insert totalCount ${this.totalCount}`)
44. .height(60)
45. .onClick(() => {
46. // 插入元素，元素位置为屏幕显示的前一个元素
47. this.arrayHolder.arr.splice(18, 0, this.totalCount);
48. this.totalCount = this.arrayHolder.arr.length;
49. })
50. }
51. .width('100%')
52. .margin({ top: 5 })
53. }
54. }
```

[RepeatTemplateSingle.ets](https://gitcode.com/HarmonyOS_Samples/guide-snippets/blob/HarmonyOS-feature-20251117/ArkUISample/RenderingControl/entry/src/main/ets/pages/RenderingRepeat/RepeatTemplateSingle.ets#L16-L71)

运行效果：

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/09/v3/hWUaAY5VT6eL15ylKRU5sA/zh-cn_image_0000002571171317.gif?HW-CC-KV=V1&HW-CC-Date=20260414T034738Z&HW-CC-Expire=86400&HW-CC-Sign=289B6BBB7967A7755F3258D585B5EA1AF1B5301CDBCE386B3F2BDEFF20E6C623)

以下为修正后的示例：

在一些场景中，我们不希望屏幕外的数据源变化影响屏幕中List列表Scroller停留的位置，可以通过List组件的[onScrollIndex](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-layout-development-create-list#响应滚动位置)事件对列表滚动动作进行监听，当列表发生滚动时，获取列表滚动位置。使用Scroller组件的[scrollToIndex](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-scroll#scrolltoindex)特性，滑动到指定index位置，实现屏幕外的数据源增加/删除数据时，Scroller停留的位置不变的效果。

示例代码仅对增加数据的情况进行展示。

收起

自动换行

深色代码主题

复制

```
1. // 定义一个类，标记为可观察的
2. // 类中自定义一个数组，标记为可追踪的
3. @ObservedV2
4. class ArrayHolderLocal {
5. @Trace public arr: Array<number> = [];

7. // constructor，用于初始化数组个数
8. constructor(count: number) {
9. for (let i = 0; i < count; i++) {
10. this.arr.push(i);
11. }
12. }
13. }
14. @Entry
15. @ComponentV2
16. struct RepeatSingle {
17. @Local arrayHolder: ArrayHolderLocal = new ArrayHolderLocal(100);
18. @Local totalCount: number = this.arrayHolder.arr.length;
19. scroller: Scroller = new Scroller();

21. private start: number = 1;
22. private end: number = 1;

24. build() {
25. Column({ space: 5 }) {
26. List({ space: 20, initialIndex: 19, scroller: this.scroller }) {
27. Repeat(this.arrayHolder.arr)
28. .virtualScroll({ totalCount: this.totalCount })
29. .templateId((item, index) => {
30. return 'number';
31. })
32. .template('number', (r) => {
33. ListItem() {
34. Text(r.index! + ':' + r.item + 'Reuse')
35. }
36. })
37. .each((r) => {
38. ListItem() {
39. Text(r.index! + ':' + r.item + 'eachMessage')
40. }
41. })
42. }
43. .onScrollIndex((start, end) => {
44. this.start = start;
45. this.end = end;
46. })
47. .height('30%')

49. Button(`insert totalCount ${this.totalCount}`)
50. .height(60)
51. .onClick(() => {
52. // 插入元素，元素位置为屏幕显示的前一个元素
53. this.arrayHolder.arr.splice(18, 0, this.totalCount);
54. let rect = this.scroller.getItemRect(this.start); // 获取子组件的大小位置
55. this.scroller.scrollToIndex(this.start + 1); // 滑动到指定index
56. this.scroller.scrollBy(0, -rect.y); // 滑动指定距离
57. this.totalCount = this.arrayHolder.arr.length;
58. })
59. }
60. .width('100%')
61. .margin({ top: 5 })
62. }
63. }
```

[RepeatTemplateSingle1.ets](https://gitcode.com/HarmonyOS_Samples/guide-snippets/blob/HarmonyOS-feature-20251117/ArkUISample/RenderingControl/entry/src/main/ets/pages/RenderingRepeat/RepeatTemplateSingle1.ets#L16-L80)

运行效果：

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/51/v3/LYdoKFMyQqWe5FD2_WoOTA/zh-cn_image_0000002540770974.gif?HW-CC-KV=V1&HW-CC-Date=20260414T034738Z&HW-CC-Expire=86400&HW-CC-Sign=78CB5FD020C0E26F02A73FFFEB70B7066E2B894D4944F25C1BD41CA42D662237)

### totalCount值大于数据源长度

当数据源总长度很大时，会使用懒加载的方式先加载一部分数据，为了使Repeat显示正确的滚动条样式，需要将数据总长度赋值给totalCount，即数据源全部加载完成前，totalCount大于array.length。

totalCount > array.length时，在父组件容器滚动过程中，应用需要保证列表即将滑动到数据源末尾时请求后续数据，开发者需要对数据请求的错误场景（如网络延迟）进行保护操作，直到数据源全部加载完成，否则列表滑动的过程中会出现滚动效果异常。

上述规范可以通过实现父组件List/Grid的[onScrollIndex](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-layout-development-create-list#响应滚动位置)属性的回调函数完成。示例代码如下：

收起

自动换行

深色代码主题

复制

```
1. @ObservedV2
2. class VehicleData {
3. @Trace public name: string;
4. @Trace public price: number;

6. constructor(name: string, price: number) {
7. this.name = name;
8. this.price = price;
9. }
10. }

12. @ObservedV2
13. class VehicleDB {
14. public vehicleItems: VehicleData[] = [];

16. constructor() {
17. // 数组初始化大小 20
18. for (let i = 1; i <= 20; i++) {
19. this.vehicleItems.push(new VehicleData(`Vehicle${i}`, i));
20. }
21. }
22. }

24. @Entry
25. @ComponentV2
26. struct EntryCompSucc {
27. @Local vehicleItems: VehicleData[] = new VehicleDB().vehicleItems;
28. @Local listChildrenSize: ChildrenMainSize = new ChildrenMainSize(60);
29. @Local totalCount: number = this.vehicleItems.length;
30. scroller: Scroller = new Scroller();

32. build() {
33. Column({ space: 3 }) {
34. List({ scroller: this.scroller }) {
35. Repeat(this.vehicleItems)
36. .virtualScroll({ totalCount: 50 }) // 数组预期长度 50
37. .templateId(() => 'default')
38. .template('default', (ri) => {
39. ListItem() {
40. Column() {
41. Text(`${ri.item.name} + ${ri.index}`)
42. .width('90%')
43. .height(this.listChildrenSize.childDefaultSize)
44. .backgroundColor(0xFFA07A)
45. .textAlign(TextAlign.Center)
46. .fontSize(20)
47. .fontWeight(FontWeight.Bold)
48. }
49. }.border({ width: 1 })
50. }, { cachedCount: 5 })
51. .each((ri) => {
52. ListItem() {
53. Text('Wrong: ' + `${ri.item.name} + ${ri.index}`)
54. .width('90%')
55. .height(this.listChildrenSize.childDefaultSize)
56. .backgroundColor(0xFFA07A)
57. .textAlign(TextAlign.Center)
58. .fontSize(20)
59. .fontWeight(FontWeight.Bold)
60. }.border({ width: 1 })
61. })
62. .key((item, index) => `${index}:${item}`)
63. }
64. .height('50%')
65. .margin({ top: 20 })
66. .childrenMainSize(this.listChildrenSize)
67. .alignListItem(ListItemAlign.Center)
68. .onScrollIndex((start, end) => {
69. // 数据懒加载
70. if (this.vehicleItems.length < 50) {
71. for (let i = 0; i < 10; i++) {
72. if (this.vehicleItems.length < 50) {
73. this.vehicleItems.push(new VehicleData('Vehicle_loaded', i));
74. }
75. }
76. }
77. })
78. }
79. }
80. }
```

[EntryCompSucc.ets](https://gitcode.com/HarmonyOS_Samples/guide-snippets/blob/HarmonyOS-feature-20251117/ArkUISample/RenderingControl/entry/src/main/ets/pages/RenderingRepeat/EntryCompSucc.ets#L16-L97)

示例代码运行效果：

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/5d/v3/yQ2d_cilQg6rDFO3RJfWlw/zh-cn_image_0000002571291271.gif?HW-CC-KV=V1&HW-CC-Date=20260414T034738Z&HW-CC-Expire=86400&HW-CC-Sign=7661F973BA658C5D8AC42D4D2E63759EAD4B99B5C1FACF4073B3777C4984AFBB)

### Repeat与@Builder混用

当Repeat与@Builder混用时，如果只传递RepeatItem.item或RepeatItem.index，参数值的改变不会引起@Builder函数内的UI刷新。推荐使用[按引用传递](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-builder#按引用传递参数)，即将RepeatItem类型整体进行传参，组件才能监听到数据变化。除此之外，从API version 20开始，开发者可以通过使用[UIUtils.makeBinding()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-statemanagement#makebinding20)函数、[Binding类](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-statemanagement#bindingt20)和[MutableBinding类](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-statemanagement#mutablebindingt20)实现@Builder函数中状态变量的刷新。

示例代码如下：

收起

自动换行

深色代码主题

复制

```
1. import { UIUtils, Binding } from '@kit.ArkUI';

3. @Entry
4. @ComponentV2
5. struct RepeatBuilderPage {
6. @Local simpleList: Array<number> = [];

8. aboutToAppear(): void {
9. for (let i = 0; i < 100; i++) {
10. this.simpleList.push(i);
11. }
12. }

14. @Builder
15. buildItem1(bindingData: Binding<number>) { // 使用Binding类/MutableBinding类接收传参，通过value属性访问值。
16. Text('[Binding] item: ' + bindingData.value)
17. .fontSize(20)
18. }

20. @Builder
21. buildItem2(ri: RepeatItem<number>) {
22. Text('[RepeatItem] item: ' + ri.item)
23. .fontSize(20)
24. }

26. @Builder
27. buildItem3(data: number) {
28. Text('[number] item: ' + data)
29. .fontSize(20).fontColor(Color.Red)
30. }

32. build() {
33. Column({ space: 10 }) {
34. List({ space: 20 }) {
35. Repeat<number>(this.simpleList)
36. .each((ri) => {
37. ListItem() {
38. Column({ space: 2 }) {
39. this.buildItem1(UIUtils.makeBinding<number>(() => ri.item)) // 使用UIUtils.makeBinding()函数实现@Builder函数中状态变量的刷新。
40. this.buildItem2(ri) // 按引用传递，状态变量的改变会引起@Builder函数内的UI刷新。
41. this.buildItem3(ri.item) // 反例。按值传递，状态变量的改变不会引起@Builder函数内的UI刷新。
42. }
43. }.border({ width: 1 })
44. }).virtualScroll()
45. }
46. .cachedCount(1)
47. .border({ width: 1 })
48. .width('70%')
49. .height('60%')
50. .alignListItem(ListItemAlign.Center)

52. Button('click to change data.').onClick(() => {
53. this.simpleList[0] = 10000; // 修改第一项数据为10000。
54. })
55. }
56. .width('100%').height('100%')
57. .justifyContent(FlexAlign.Center)
58. }
59. }
```

@Builder传参方式依次为makeBinding()、地址传递和值传递，界面展示如下图，进入页面后点击按钮改变数据。在@Builder构造函数中使用值传递传参不会引起函数内的UI刷新。

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/7/v3/tnU5-n2gQJq7NSyy8Pasnw/zh-cn_image_0000002540611324.png?HW-CC-KV=V1&HW-CC-Date=20260414T034738Z&HW-CC-Expire=86400&HW-CC-Sign=B35482FDC1A88BAC482606CB5BAADA9BBB3DBB79C0CFA9100187B6ED687583C2)

### Repeat子组件声明expandSafeArea属性时，子组件无法扩展到全屏

在API version 18之前，Repeat子组件声明expandSafeArea属性，子组件无法扩展至全屏；从API version 18开始，子组件声明expandSafeArea属性可正常扩展至全屏展示。