用 ArkTS 的 Repeat 组件玩转正向循环渲染:从基础到实战

如果你经常开发 HarmonyOS 应用,肯定遇到过长列表渲染的问题 —— 数据太多时页面卡得动不了,滑动时一顿一顿的,用户体验贼差。别担心,ArkTS 的 Repeat 组件就是来解决这个问题的!它就像个 "智能管家",能按需加载组件、自动回收复用,让长列表滑动如丝般顺滑。今天咱们就用大白话聊聊 Repeat 怎么用,从基础用法到高级技巧,保证看完你就能上手~

一、Repeat 是啥?先搞懂它的核心优势

咱们先拿生活中的例子打比方:如果把列表渲染比作 "摆地摊",传统方式是不管顾客要不要,先把所有商品都摆出来,占地儿又费力;而 Repeat 就像 "流动摊位",顾客走到哪就把对应商品摆出来,卖完就收起来,效率高多了。

重点总结
Repeat 是基于数组的循环渲染组件,专为长列表优化,核心优势是 "按需加载" 和 "节点复用",能大幅提升滚动性能。它比老款的 LazyForEach 更智能 —— 不用手动实现数据源接口,还支持多模板渲染,用起来更简单。

为啥不用 LazyForEach?对比看差异

特性 Repeat LazyForEach
数据监听 自动监听状态变化 需手动实现 IDataSource 接口
节点复用 自动复用,性能更好 复用能力弱
多模板 支持不同类型子组件 不支持
用法复杂度 简单,直接绑定数组 复杂,需写额外接口逻辑

简单说,Repeat 就是 LazyForEach 的 "升级版",能少写很多代码,还更高效~

二、入门:Repeat 的基础用法,3 分钟上手

Repeat 必须和滚动容器(List、Grid、Swiper 等)搭配使用,最常用的就是 List。咱们先从最简单的例子开始,用 Repeat 渲染一个 50 条数据的列表。

重点总结
基础用法只需 3 步:1. 准备数据源数组;2. 在 List 里嵌套 Repeat;3. 用.each () 定义子组件样式。记得加.virtualScroll () 开启懒加载,不然就失去性能优势了。

@Entry
@ComponentV2
struct RepeatBasicDemo {
  // 准备数据源:50条字符串数据
  @Local dataList: string[] = [];

  // 页面加载前初始化数据
  aboutToAppear() {
    for (let i = 0; i < 50; i++) {
      this.dataList.push(`第${i}条数据`);
    }
  }

  build() {
    Column() {
      Text("基础Repeat列表")
        .fontSize(20)
        .margin(10)

      // 滚动容器用List
      List() {
        // 核心:用Repeat循环渲染
        Repeat(this.dataList)
          // 定义每个子组件的样式
          .each((item: RepeatItem) => {
            ListItem() {
              Text(item.item)  // 显示数组中的内容
                .fontSize(16)
                .padding(15)
                .width("100%")
                .backgroundColor("#f5f5f5")
                .borderRadius(5)
            }
          })
          // 开启懒加载,指定总数据量
          .virtualScroll({ totalCount: this.dataList.length })
    }
    .padding(10)
    .width("100%")
    .height("100%")
  }
}

这段代码里,Repeat 会自动监听dataList的变化,只加载屏幕可视区域 + 预加载区域的内容。滚动时,它会把离开视野的组件回收,新进入的组件复用之前的 "壳子",性能比一次性渲染所有组件好太多!

三、进阶:多模板渲染,让列表样式更灵活

有时候列表里需要显示不同样式的内容,比如前 5 条是红色标题,中间 10 条是蓝色正文,剩下的是灰色备注。这时候 Repeat 的模板功能就派上用场了,就像给不同数据 "穿不同的衣服"。

重点总结
用.templateId () 给数据分类型,再用.template () 定义每种类型的样式,默认样式用.each ()。同一类型的组件会被优先复用,进一步提升性能。

@Entry
@ComponentV2
struct RepeatTemplateDemo {
  @Local dataList: string[] = [];

  aboutToAppear() {
    for (let i = 0; i < 30; i++) {
      this.dataList.push(`内容${i}`);
    }
  }

  build() {
    Column() {
      Text("多模板Repeat示例")
        .fontSize(20)
        .margin(10)

      List() {
        Repeat(this.dataList)
          // 1. 给数据分类(返回模板类型)
          .templateId((item: string, index: number) => {
            if (index < 5) return "title";    // 前5条用title模板
            if (index < 15) return "content"; // 中间10条用content模板
            return "";                        // 剩下的用默认模板
          })
          // 2. 定义title模板(红色大字体)
          .template("title", (item: RepeatItem) => {
            ListItem() {
              Text(`【标题】${item.item}`)
                .fontSize(18)
                .fontColor("#ff4444")
                .padding(15)
                .width("100%")
                .backgroundColor("#fff0f0")
            }
          }, { cachedCount: 3 }) // 缓存3个title组件备用

          // 3. 定义content模板(蓝色中字体)
          .template("content", (item: RepeatItem) => {
            ListItem() {
              Text(`【正文】${item.item}`)
                .fontSize(16)
                .fontColor("#4444ff")
                .padding(15)
                .width("100%")
                .backgroundColor("#f0f0ff")
            }
          }, { cachedCount: 5 }) // 缓存5个content组件备用

          // 4. 默认模板(灰色小字体)
          .each((item: RepeatItem) => {
            ListItem() {
              Text(`【备注】${item.item}`)
                .fontSize(14)
                .fontColor("#888888")
                .padding(15)
                .width("100%")
                .backgroundColor("#f5f5f5")
            }
          })
          .virtualScroll({ totalCount: this.dataList.length })
      }
    }
    .padding(10)
    .width("100%")
    .height("100%")
  }
}

运行后你会发现:前 5 条是红色标题,中间 10 条是蓝色正文,剩下的是灰色备注。滚动时,同类组件会被复用,避免重复创建,这就是为什么它比普通循环高效~

四、核心性能优化:节点复用和懒加载

很多人好奇:"同样是循环渲染,为啥 Repeat 滑动更流畅?" 关键就在 "节点复用" 和 "懒加载" 这两个黑科技。咱们拆开来讲:

1. 节点复用:组件也能 "回收再利用"

想象一下:你滑动列表时,顶部的组件划出屏幕,Repeat 不会把它销毁,而是放进 "缓存池";当底部需要新组件时,直接从缓存池里拿一个改改内容再用,省去了创建新组件的耗时。这就像奶茶店的杯子 —— 用过的洗干净再装新奶茶,比每次都用新杯子快多了。

重点总结
Repeat 默认开启节点复用,相同模板类型的组件会被优先复用。从 API 18 开始,可通过reusable字段关闭,但建议一直开着,性能提升很明显。

// 控制复用功能的示例(API 18+)
Repeat(this.dataList)
  .virtualScroll({
    totalCount: this.dataList.length,
    reusable: true // 开启复用(默认值),设为false关闭
  })
  // ...其他配置

2. 懒加载:按需加载,不浪费资源

如果列表有 1000 条数据,一次性加载完会卡死。Repeat 的懒加载就像 "点外卖"—— 先加载当前能看到的,滑动到哪再加载哪,内存占用少,启动速度快。

重点总结
用.virtualScroll () 开启懒加载,通过totalCount告诉 Repeat 总共有多少数据。数据量特别大时,还能通过onLazyLoading动态加载数据,实现 "无限滚动"。

案例:实现无限滚动列表
@Entry
@ComponentV2
struct RepeatInfiniteScroll {
  @Local dataList: string[] = [];
  // 初始加载15条数据(首屏够用)
  aboutToAppear() {
    for (let i = 0; i < 15; i++) {
      this.dataList.push(`初始数据${i}`);
    }
  }

  build() {
    Column() {
      Text("无限滚动示例")
        .fontSize(20)
        .margin(10)

      List() {
        Repeat(this.dataList)
          .virtualScroll({
            // 总数据量设为当前长度+1,实现"无限"
            onTotalCount: () => this.dataList.length + 1,
            // 当需要加载新数据时触发
            onLazyLoading: (index: number) => {
              // 模拟网络请求延迟
              setTimeout(() => {
                this.dataList[index] = `新加载数据${index}`;
              }, 500);
            }
          })
          .each((item: RepeatItem) => {
            ListItem() {
              Text(item.item)
                .fontSize(16)
                .padding(15)
                .width("100%")
                .backgroundColor("#f5f5f5")
            }
          })
          .key((item: string) => item) // 用数据内容当唯一标识
      }
    }
    .padding(10)
    .width("100%")
    .height("100%")
  }
}

这段代码会在你滑动到列表底部时,自动加载新数据,实现类似朋友圈 "下拉加载更多" 的效果,而且全程流畅不卡顿~

3. 精准懒加载:数据太多?分批加载!

如果数据总量很大(比如 10000 条),或者加载每条数据都很耗时(比如从网络拉取),可以用精准懒加载 —— 只在需要显示的时候才加载对应位置的数据,初始化时不用加载全部。

重点总结
通过onLazyLoading回调,在组件首次渲染、滑动到对应位置时才加载数据,还能设置占位符优化体验。

@Entry
@ComponentV2
struct RepeatPreciseLazyLoad {
  @Local dataList: string[] = []; // 初始为空数组

  build() {
    Column() {
      Text("精准懒加载示例(1000条数据)")
        .fontSize(20)
        .margin(10)

      List({ initialIndex: 100 }) { // 初始定位到第100条
        Repeat(this.dataList)
          .virtualScroll({
            onTotalCount: () => 1000, // 总共有1000条数据
            onLazyLoading: (index: number) => {
              // 先显示占位符
              this.dataList[index] = "加载中...";
              // 模拟网络请求,1秒后加载真实数据
              setTimeout(() => {
                this.dataList[index] = `第${index}条数据(懒加载)`;
              }, 1000);
            }
          })
          .each((item: RepeatItem) => {
            ListItem() {
              Text(item.item)
                .fontSize(16)
                .padding(15)
                .width("100%")
                .backgroundColor(item.item === "加载中..." ? "#ffebee" : "#f5f5f5")
            }
          })
          .key((item: string, index: number) => index.toString())
      }
      .height("80%")
      .border({ width: 1 })
    }
    .padding(10)
    .width("100%")
    .height("100%")
  }
}

运行后你会发现:虽然要显示 1000 条数据,但初始化时几乎瞬间完成,滑动到哪条才加载哪条,还会显示 "加载中..." 的占位符,用户体验拉满~

五、高级功能:拖拽排序、前插保持

Repeat 还有些超实用的高级功能,比如让列表项支持拖拽排序,或者在列表前面插入数据时保持当前视图不变,这些在实际开发中特别常用。

1. 拖拽排序:长按拖动,轻松调整顺序

想实现类似 "待办清单" 那样,长按列表项就能拖动调整顺序的功能?Repeat 在 List 里配合onMove事件就能轻松搞定。

重点总结
在 List 中使用 Repeat 时,设置onMove事件,在回调中修改数据源的顺序即可实现拖拽排序,记得保持 key 值不变以保证动画正常。

@Entry
@ComponentV2
struct RepeatDragSort {
  @Local todoList: string[] = [];

  aboutToAppear() {
    // 初始化10条待办事项
    for (let i = 0; i < 10; i++) {
      this.todoList.push(`待办事项${i}`);
    }
  }

  build() {
    Column() {
      Text("拖拽排序示例(长按拖动)")
        .fontSize(20)
        .margin(10)

      List() {
        Repeat(this.todoList)
          // 启用拖拽排序
          .onMove((from: number, to: number) => {
            // 从from位置删除,插入到to位置
            const [item] = this.todoList.splice(from, 1);
            this.todoList.splice(to, 0, item);
          })
          .each((item: RepeatItem) => {
            ListItem() {
              Text(item.item)
                .fontSize(18)
                .padding(20)
                .width("100%")
                .backgroundColor("#ffffff")
                .borderRadius(8)
                .shadow({ radius: 2 })
            }
            .margin(5)
          })
          .key((item: string) => item) // 用内容当唯一key
          .virtualScroll({ totalCount: this.todoList.length })
      }
      .backgroundColor("#f5f5f5")
      .width("100%")
      .height("100%")
    }
  }
}

这段代码实现的列表项,长按后可以拖动到任意位置,松手后自动更新顺序,整个过程丝滑流畅,比自己写拖拽逻辑简单多了~

2. 前插保持:在列表前面加数据,视图不变

你有没有遇到过这种情况:在列表顶部插入新数据后,整个列表突然向上跳,原来看到的内容不见了。Repeat 的 "前插保持" 功能就能解决这个问题 —— 在前面插入数据后,当前显示的内容位置不变。

重点总结
父容器为 List 时,设置maintainVisibleContentPosition: true,在列表前面插入 / 删除数据时,当前显示区域的内容位置保持不变。

@Entry
@ComponentV2
struct RepeatPreInsertKeep {
  @Local messageList: string[] = [];
  private count: number = 100; // 用于生成新数据

  aboutToAppear() {
    // 初始化30条数据
    for (let i = 0; i < 30; i++) {
      this.messageList.push(`消息${i}`);
    }
  }

  build() {
    Column() {
      // 操作按钮
      Row({ space: 10 }) {
        Button("在顶部插入新消息")
          .onClick(() => {
            this.messageList.unshift(`新消息${this.count++}`);
          })
        Button("删除第一条消息")
          .onClick(() => {
            this.messageList.shift();
          })
      }
      .margin(10)

      // 列表(启用前插保持)
      List({ initialIndex: 5 }) {
        Repeat(this.messageList)
          .each((item: RepeatItem) => {
            ListItem() {
              Text(`序号${item.index}:${item.item}`)
                .fontSize(16)
                .padding(15)
                .width("100%")
                .backgroundColor("#ffffff")
            }
            .margin(5)
          })
          .key((item: string) => item)
          .virtualScroll({ totalCount: this.messageList.length })
      }
      .maintainVisibleContentPosition(true) // 启用前插保持
      .backgroundColor("#f5f5f5")
      .width("100%")
      .height("80%")
    }
  }
}

点击 "在顶部插入新消息" 按钮后,你会发现:虽然列表顶部多了新数据,但当前看到的内容位置没变,不会突然跳转,用户体验瞬间提升~

六、实战:把 Repeat 和常见容器组件搭配合

Repeat 不是孤军奋战,它和 List、Grid、Swiper 等容器组件是黄金搭档,不同组合能实现不同效果,咱们看几个实战案例:

1. 与 List 搭配:实现带滑动删除的列表

很多应用需要 "左滑删除" 功能,比如短信列表。用 List+Repeat 很容易实现:

// 定义数据模型
class Message {
  id: string;
  content: string;
  constructor(id: string, content: string) {
    this.id = id;
    this.content = content;
  }
}

@Entry
@ComponentV2
struct RepeatWithList {
  @Local messages: Message[] = [];

  aboutToAppear() {
    // 初始化10条消息
    for (let i = 0; i < 10; i++) {
      this.messages.push(new Message(`msg${i}`, `这是第${i}条消息`));
    }
  }

  // 删除消息的方法
  deleteMessage(index: number) {
    this.messages.splice(index, 1);
  }

  build() {
    Column() {
      Text("带滑动删除的消息列表")
        .fontSize(20)
        .margin(10)

      List() {
        Repeat(this.messages)
          .each((item: RepeatItem) => {
            ListItem() {
              Text(item.item.content)
                .fontSize(16)
                .padding(15)
                .width("100%")
            }
            // 右滑显示删除按钮
            .swipeAction({
              end: {
                builder: () => {
                  Button("删除")
                    .backgroundColor("#ff4444")
                    .onClick(() => this.deleteMessage(item.index))
                }
              }
            })
          })
          .key((item: Message) => item.id) // 用id当唯一标识
          .virtualScroll({ totalCount: this.messages.length })
      }
      .width("100%")
      .height("100%")
    }
  }
}

右滑列表项会出现删除按钮,点击后对应项被删除,列表自动刷新,整个交互流畅自然~

2. 与 Grid 搭配:实现图片瀑布流

电商 App 常用的瀑布流布局,用 Grid+Repeat 也能轻松实现,还能支持下拉刷新:

// 定义商品数据模型
class Product {
  id: string;
  name: string;
  imgUrl: string;
  constructor(id: string, name: string) {
    this.id = id;
    this.name = name;
    this.imgUrl = `https://picsum.photos/200/300?random=${id}`; // 随机图片
  }
}

@Entry
@ComponentV2
struct RepeatWithGrid {
  @Local products: Product[] = [];
  @Local isRefreshing: boolean = false;

  aboutToAppear() {
    // 初始化20个商品
    for (let i = 0; i < 20; i++) {
      this.products.push(new Product(`p${i}`, `商品${i}`));
    }
  }

  build() {
    Column() {
      Text("商品瀑布流(下拉刷新)")
        .fontSize(20)
        .margin(10)

      // 下拉刷新容器
      Refresh({ refreshing: $$this.isRefreshing }) {
        Grid() {
          Repeat(this.products)
            .each((item: RepeatItem) => {
              GridItem() {
                Column() {
                  Image(item.item.imgUrl)
                    .width("100%")
                    .height(150)
                    .objectFit(ImageFit.Cover)
                  Text(item.item.name)
                    .fontSize(14)
                    .padding(5)
                }
                .backgroundColor("#ffffff")
                .borderRadius(8)
                .shadow({ radius: 2 })
              }
              .padding(5)
            })
            .key((item: Product) => item.id)
            .virtualScroll({ totalCount: this.products.length })
        }
        .columnsTemplate("repeat(auto-fit, 150)") // 自适应列数
        .rowsGap(10)
        .columnsGap(10)
        .padding(10)
        .backgroundColor("#f5f5f5")
      }
      .onRefreshing(() => {
        // 模拟刷新数据
        setTimeout(() => {
          this.products.unshift(new Product(`new${Date.now()}`, "最新商品"));
          this.isRefreshing = false;
        }, 1000);
      })
    }
    .width("100%")
    .height("100%")
  }
}

这个瀑布流会根据屏幕宽度自动调整列数,下拉时会刷新并添加新商品,图片加载和滑动都很流畅~

3. 与 Swiper 搭配:实现图片轮播

首页轮播图是 App 的常见组件,用 Swiper+Repeat 实现,还能支持懒加载图片:

// 轮播图数据
const bannerUrls = [
  "https://picsum.photos/800/400?random=1",
  "https://picsum.photos/800/400?random=2",
  "https://picsum.photos/800/400?random=3",
  "https://picsum.photos/800/400?random=4",
  "https://picsum.photos/800/400?random=5",
];

@Entry
@ComponentV2
struct RepeatWithSwiper {
  @Local banners: { id: string; url: string }[] = [];

  aboutToAppear() {
    // 初始化轮播数据(先不设置图片地址,等滑动时加载)
    bannerUrls.forEach((url, i) => {
      this.banners.push({ id: `b${i}`, url: "" });
    });
  }

  build() {
    Column() {
      Text("图片轮播(懒加载)")
        .fontSize(20)
        .margin(10)

      Swiper() {
        Repeat(this.banners)
          .each((item: RepeatItem<{ id: string; url: string }>) => {
            Image(item.item.url || "https://picsum.photos/800/400?blur=2") // 加载前显示模糊占位图
              .width("100%")
              .height(200)
              .objectFit(ImageFit.Cover)
          })
          .key((item) => item.id)
          .virtualScroll({ totalCount: this.banners.length })
      }
      .indicator(true) // 显示指示器
      .loop(true) // 循环播放
      .onChange((index) => {
        // 滑动到对应页时才加载图片(模拟网络延迟)
        setTimeout(() => {
          this.banners[index].url = bannerUrls[index];
        }, 500);
      })
    }
    .width("100%")
    .height("100%")
  }
}

这个轮播图会在滑动到对应页面时才加载图片,节省流量,还会显示占位图,用户体验更友好~

七、避坑指南:这些错误别再犯了!

用 Repeat 时,很多人会踩坑导致效果异常,咱们总结几个常见问题和解决方案:

1. 与 @Builder 混用时,传参要传整个 RepeatItem

很多人在自定义组件里用 @Builder 时,只传item.itemitem.index,结果导致 UI 不更新。正确做法是把整个RepeatItem对象传过去。

错误示例

@Builder
buildItem(itemContent: string) { // 只传了内容,无法监听变化
  Text(itemContent).fontSize(16);
}

// 使用时
.template("default", (ri) => {
  this.buildItem(ri.item); // 错误!
})

正确示例

@Builder
buildItem(ri: RepeatItem) { // 传整个RepeatItem
  Text(ri.item).fontSize(16);
}

// 使用时
.template("default", (ri) => {
  this.buildItem(ri); // 正确!
})

2. totalCount 要和实际数据长度匹配

如果totalCount比实际数据长太多,滑动到后面会出现空白;如果比实际短,会显示不全。正确做法是:数据加载完前,totalCount设为预期总长度;加载完后,设为实际长度。

// 正确设置totalCount的示例
Repeat(this.dataList)
  .virtualScroll({
    totalCount: this.isLoading ? 100 : this.dataList.length // 加载中设为100,完成后用实际长度
  })

3. key 值别用 index,否则复用出问题

很多人图省事,用index当 key 值,结果数据顺序变化时,Repeat 以为数据变了,导致组件重新渲染,性能下降。正确做法是用数据中唯一不变的字段(如 id)当 key。

推荐写法

.key((item: Product) => item.id) // 用唯一id当key

4. 关闭懒加载只适合短列表

当你不需要懒加载(即不写.virtualScroll()),Repeat 会一次性加载所有组件,适合数据量少(少于 30 条)的场景。长列表用这个会卡顿!

// 短列表可以关闭懒加载(不写virtualScroll)
Repeat(shortList)
  .each((item) => { ... })
  .key((item) => item.id)

八、总结:Repeat 到底好在哪?

看到这里,你应该对 Repeat 有全面了解了。咱们再总结下它的核心优势:

  1. 性能好:按需加载 + 节点复用,长列表滑动如丝滑
  2. 用法简单:不用手动实现数据源接口,直接绑定数组
  3. 功能强:支持多模板、拖拽排序、前插保持等高级功能
  4. 兼容性好:能和 List、Grid、Swiper 等容器完美配合

下次开发列表类功能时,别再用 ForEach 硬扛了,试试 Repeat 组件,你会发现 HarmonyOS 应用开发原来可以这么轻松~ 赶紧动手试试吧!

你可能感兴趣的:(harmonyos,华为,深度学习)