前言

鸿蒙的更新还是很快的,一不注意,就有性能提升的组件或者api 出现。从一开始api9,一直到现在,其实经历了很多,比如List组件,最开始使用的是ForEach来循环子组件,接着如果是长列表为了更好的性能,就需要使用LazyForEach,不过它有点难用,直到现在又出现了新的 Repeat。
所以呢,如果新开发的页面中涉及列表,直接用Repeat,使用起来也非常简单。这就好比,在android开发的时候,使用ListView 和 使用 Recyclerview的感觉。当有了RecyclerView,可以直接放弃ListView,因为RecyclerView 性能更好,而且覆盖了ListView的功能。同理,鸿蒙有了Repeat,写列表相关的页面时直接用 List + Repeat 即可。

一、Repeat

1、概述

Repeat基于数组类型数据来进行循环渲染,一般与容器组件配合使用。Repeat根据容器组件的有效加载范围(屏幕可视区域+预加载区域)加载子组件。当容器滑动/数组改变时,Repeat会根据父容器组件的布局过程重新计算有效加载范围,并管理列表子组件节点的创建与销毁。Repeat通过组件节点更新/复用从而优化性能表现。

2、使用限制

  • Repeat必须在滚动类容器组件内使用,仅有List、Grid、Swiper 以及 WaterFlow 组件支持 Repeat 懒加载场景。循环渲染只允许创建一个子组件,子组件应当是允许包含在容器组件中的子组件。例如:Repeat 与 List 组件配合使用时,子组件必须为 ListItem 组件。
  • Repeat不支持V1装饰器,混用V1装饰器会导致渲染异常。
  • Repeat当前不支持动画效果。
  • 滚动容器组件内只能包含一个Repeat。以List为例,同时包含ListItem、ForEach、LazyForEach的场景是不推荐的;同时包含多个Repeat也是不推荐的。
  • 当Repeat与自定义组件或@Builder函数混用时,必须将RepeatItem类型整体进行传参,组件才能监听到数据变化。

二、使用说明

Repeat子组件由.each()和.template()属性定义,只允许包含一个子组件。当页面首次渲染时,Repeat根据当前的有效加载范围(屏幕可视区域+预加载区域)按需创建子组件

1、循环渲染能力

.each()适用于只需要循环渲染一种子组件的场景

代码如下(示例):

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

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

  build() {
    Column() {
      List() {
        Repeat<string>(this.dataArr)
          .each((ri: RepeatItem<string>) => {
            ListItem() {
              Text('each_' + ri.item).fontSize(30)
            }
          })
          .virtualScroll({ totalCount: this.dataArr.length }) // 打开懒加载,totalCount为期望加载的数据长度
      }
      .cachedCount(2) // 容器组件的预加载区域大小
      .height('70%')
      .border({ width: 1 }) // 边框
    }
  }
}

.template()适用于在同一个数据源中渲染多种子组件

每个数据项会根据.templateId()得到template type,从而渲染type对应的.template()中的子组件。

  • 如果.templateId()缺省,则type默认为空字符串。
  • 当多个template type相同时,Repeat会覆盖先定义的.template()函数,仅生效最后定义的.template()。
  • 如果找不到对应的template type,Repeat会先渲染type为空的.template()中的子组件,如果没有则渲染.each()中的子组件。
  • 只有相同template的节点可以互相复用。

代码如下(示例):

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

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

  build() {
    Column() {
      List() {
        Repeat<string>(this.dataArr)
          .each((ri: RepeatItem<string>) => { // 默认渲染模板
            ListItem() {
              Text('each_' + ri.item).fontSize(30).fontColor('rgb(161,10,33)') // 文本颜色为红色
            }
          })
          .key((item: string, index: number): string => JSON.stringify(item)) // 键值生成函数
          .virtualScroll({ totalCount: this.dataArr.length }) // 打开懒加载,totalCount为期望加载的数据长度
          .templateId((item: string, index: number): string => { // 根据返回值寻找对应的模板子组件进行渲染
            return index <= 4 ? 'A' : (index <= 10 ? 'B' : ''); // 前5个节点模板为A,接下来的5个为B,其余为默认模板
          })
          .template('A', (ri: RepeatItem<string>) => { // 'A'模板
            ListItem() {
              Text('A_' + ri.item).fontSize(30).fontColor('rgb(23,169,141)') // 文本颜色为绿色
            }
          }, { cachedCount: 3 }) // 'A'模板的缓存列表容量为3
          .template('B', (ri: RepeatItem<string>) => { // 'B'模板
            ListItem() {
              Text('B_' + ri.item).fontSize(30).fontColor('rgb(39,135,217)') // 文本颜色为蓝色
            }
          }, { cachedCount: 4 }) // 'B'模板的缓存列表容量为4
      }
      .cachedCount(2) // 容器组件的预加载区域大小
      .height('70%')
    }
  }
}

2、条件渲染

通过if/else 在each中可显示不同模板,实现条件化渲染

   .each((item: RepeatItem<number>)=>{
          ListItem(){
              //条件渲染,不同的条件显示不同的布局
              if (item.index % 2 === 0) {
                Text('' + item.item)
                  .textAlign(TextAlign.Center)
                  .width('90%')
                  .height(72)
                  .backgroundColor('#FFFFFF')
                  .borderRadius(24)
              }else{
                Text('我是奇数布局' + item.item)
                  .textAlign(TextAlign.Center)
                  .width('90%')
                  .height(72)
                  .backgroundColor('#FFFFFF')
                  .borderRadius(24)
              }
            }
          })

3、VirtualScrollOptions对象说明

  • totalCount:期望加载的数据长度,默认为原数组长度,可以大于已加载数据项的数量。假设 arr.length 表示数据源长度,则
    totalCount 缺省或是非自然数时,totalCount 默认为 arr.length,列表正常滚动。
    0 <= totalCount < arr.length 时,界面中只渲染区间 [0, totalCount - 1] 范围内的数据。
    totalCount > arr.length 时,代表 Repeat 将渲染区间 [0, totalCount - 1] 范围内的数据,滚动条样式根据 totalCount 值变化。

  • onLazyLoading:数据精准懒加载

  • onTotalCount:计算期望的数据长度
    详细说明参考:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-rendering-control-repeat#virtualscrolloptions%E5%AF%B9%E8%B1%A1%E8%AF%B4%E6%98%8E

// arr是Array<string>类型的数组,在List容器组件中使用Repeat,并打开virtualScroll
// 将加载的数据项总数设为数据源的长度,并开启复用功能
List() {
  Repeat<string>(this.arr)
    .each((obj: RepeatItem<string>) => { ListItem() { Text(obj.item) }})
    .virtualScroll( { totalCount: this.arr.length, reusable: true } )
}


// 假设数据项总数为100,首屏渲染需3项数据
// 初始数组提供前3项数据(arr = ['No.0', 'No.1', 'No.2']),并开启数据懒加载功能
List() {
  Repeat<string>(this.arr)
    .each((obj: RepeatItem<string>) => { ListItem() { Text(obj.item) }})
    .virtualScroll({ 
      onTotalCount: () => { return 100; },
      onLazyLoading: (index: number) => { this.arr[index] = `No.${index}`; }
    })
}

4、Repeat性能上的优化

  • key 函数确保列表高效复用
  • virtualScroll 启用懒加载,提升长列表性能
  • 模板复用

5、注意事项

使用 Repeat 组件确保是在V2中使用,避免出现问题。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐