鸿蒙HarmonyOS开发:懒加载(LazyForEach)、瀑布流组件(WaterFlow)

一、简介(LazyForEach)

懒加载LazyForEach是一种延迟加载的技术,它是在需要的时候才加载数据或资源,并在每次迭代过程中创建相应的组件,而不是一次性将所有内容都加载出来。懒加载通常应用于长列表、网格、瀑布流等数据量较大、子组件可重复使用的场景,当用户滚动页面到相应位置时,才会触发资源的加载,以减少组件的加载时间,提高应用性能,提升用户体验。

二、原理介绍

在声明式描述语句中,有两种方式控制列表、网格等容器类组件的渲染,分别为循环渲染(ForEach)和数据懒加载(LazyForEach)。

1、循环渲染(ForEach)
  • 从列表数据源一次性加载全量数据。

  • 为列表数据的每一个元素都创建对应的组件,并全部挂载在组件树上。即,ForEach遍历多少个列表元素,就创建多少个ListItem组件节点并依次挂载在List组件树根节点上。

  • 列表内容显示时,只渲染屏幕可视区内的ListItem组件,可视区外的ListItem组件滑动进入屏幕内时,因为已经完成了数据加载和组件创建挂载,直接渲染即可。

其数据加载、组件树挂载、页面渲染的示意图如下所示:

在这里插入图片描述

如果列表数据较少,数据一次性全量加载不是性能瓶颈时,可以直接使用ForEach;但是当数据量大、组件结构复杂的情况下ForEach会出现性能瓶颈。这是因为要一次性加载所有的列表数据,创建所有组件节点并完成组件树的构建,在数据量大时会非常耗时,从而导致页面启动时间过长。另外,屏幕可视区外的组件虽然不会显示在屏幕上,但是仍然会占用内存。在系统处于高负载的情况下,更容易出现性能问题,极限情况下甚至会导致应用异常退出。

2、数据懒加载(LazyForEach)
  • LazyForEach会根据屏幕可视区能够容纳显示的组件数量按需加载数据。

  • 根据加载的数据量创建组件,挂载在组件树上,构建出一棵短小的组件树。即,屏幕可以展示多少列表项组件,就按需创建多少个ListItem组件节点挂载在List组件树根节点上。

  • 屏幕可视区只展示部分组件。当可视区外的组件需要在屏幕内显示时,需要从头完成数据加载、组件创建、挂载组件树这一过程,直至渲染到屏幕上。

其数据加载、组件树挂载、页面渲染的示意图如下所示:

在这里插入图片描述

LazyForEach实现了按需加载,针对列表数据量大、列表组件复杂的场景,减少了页面首次启动时一次性加载数据的时间消耗,减少了内存峰值。不过在长列表滑动的过程中,因为需要根据用户的滑动行为不断地加载新的内容,这需要进行额外的数据请求和处理,会增加滑动时的计算量,从而对性能产生一定的影响。然而,合理使用LazyForEach的按需加载能力,通过在滑动停止或达到某个阈值时才进行加载,可以减少不必要的计算和请求,从而提高性能,给用户带来更好的体验。

三、使用方法(LazyForEach)

1、接口描述
LazyForEach(
    dataSource: IDataSource,             // 需要进行数据迭代的数据源
    itemGenerator: (item: any, index: number) => void,  // 子组件生成函数
    keyGenerator?: (item: any, index: number) => string // 键值生成函数
): void
2、参数
参数名参数类型必填参数描述
dataSourceIDataSourceLazyForEach数据源,需要开发者实现相关接口。
itemGenerator(item: any, index:number) => void子组件生成函数,为数组中的每一个数据项创建一个子组件。
keyGenerator(item: any, index:number) => string键值生成函数,用于给数据源中的每一个数据项生成唯一且固定的键值。
3、IDataSource类型说明
interface IDataSource {
    totalCount(): number; // 获得数据总数
    getData(index: number): Object; // 获取索引值对应的数据
    registerDataChangeListener(listener: DataChangeListener): void; // 注册数据改变的监听器
    unregisterDataChangeListener(listener: DataChangeListener): void; // 注销数据改变的监听器
}
4、DataChangeListener类型说明
interface DataChangeListener {
    onDataReloaded(): void; // 重新加载数据完成后调用
    onDataAdded(index: number): void; // 添加数据完成后调用
    onDataMoved(from: number, to: number): void; // 数据移动起始位置与数据移动目标位置交换完成后调用
    onDataDeleted(index: number): void; // 删除数据完成后调用
    onDataChanged(index: number): void; // 改变数据完成后调用
    onDataAdd(index: number): void; // 添加数据完成后调用
    onDataMove(from: number, to: number): void; // 数据移动起始位置与数据移动目标位置交换完成后调用
    onDataDelete(index: number): void; // 删除数据完成后调用
    onDataChange(index: number): void; // 改变数据完成后调用
}

四、使用限制(LazyForEach)

  • LazyForEach必须在容器组件内使用,仅有List、Grid、Swiper以及WaterFlow组件支持数据懒加载(可配置cachedCount属性,即只加载可视部分以及其前后少量数据用于缓冲),其他组件仍然是一次性加载所有的数据。

  • LazyForEach在每次迭代中,必须创建且只允许创建一个子组件。

  • 生成的子组件必须是允许包含在LazyForEach父容器组件中的子组件。

  • 允许LazyForEach包含在if/else条件渲染语句中,也允许LazyForEach中出现if/else条件渲染语句。

  • 键值生成器必须针对每个数据生成唯一的值,如果键值相同,将导致键值相同的UI组件渲染出现问题。

  • LazyForEach必须使用DataChangeListener对象来进行更新,第一个参数dataSource使用状态变量时,状态变量改变不会触发LazyForEach的UI刷新。

  • 为了高性能渲染,通过DataChangeListener对象的onDataChange方法来更新UI时,需要生成不同于原来的键值来触发组件刷新。

五、使用场景(LazyForEach)

LazyForEach作为常见的渲染控制的方式之一,常用的使用场景有长列表加载、无限瀑布流等。

1、长列表加载

长列表作为应用开发中最常见的开发场景之一,通常会包含成千上万个列表项,在此场景下,直接使用循环渲染ForEach一次性加载所有的列表项,会导致渲染时间过长,影响用户体验。而使用数据懒加载LazyForEach替换循环渲染ForEach,可以按需加载列表项,从而提升列表性能。

虽然,按需加载列表项可以优化长列表性能,但在快速滑动长列表的场景下,可能会来不及加载需要显示的列表项,导致出现白块的现象,从而影响用户体验。而在ArkUI中,List容器提供了cachedCount属性,LazyForEach可以结合cachedCount属性一起使用,能够避免白块的现象。cachedCount可以设置列表中ListItem/ListItemGroup的预加载数量,并且只在LazyForEach中生效,即cachedCount只能与LazyForEach一起使用。除了List容器,其他容器Grid、Swiper以及WaterFlow也都包含cachedCount属性。cachedCount的使用方法如下所示。

List() {
  // ...
}.cachedCount(3)
2、无限瀑布流

瀑布流的内容呈现方式类似瀑布流一样,从上往下依次排列,每一列的高度不一定相同,整体呈现出瀑布流的视觉效果。在瀑布流中,经常使用LazyForEach实现数据按需加载,同时,结合onReachEnd、onApear方法实现无限瀑布流。

虽然在onReachEnd()触发时新增数据可以实现无限加载,但在滑动到底部时,会有明显的停顿加载新数据的过程。

想要流畅的进行无限滑动,还需要调整下增加新数据的时机。比如可以在LazyForEach还剩若干个数据就迭代到结束的情况下提前增加一些新数据。

六、瀑布流组件(WaterFlow)

瀑布流容器,由“行”和“列”分割的单元格所组成,通过容器自身的排列规则,将不同大小的“项目”自上而下,如瀑布般紧密布局。

1、接口
WaterFlow(options?: {footer?: CustomBuilder, scroller?: Scroller})
2、参数
参数名参数类型必填参数描述
footerCustomBuilder设置WaterFlow尾部组件。
scrollerscroller可滚动组件的控制器,与可滚动组件绑定。
3、属性
名称参数类型描述
columnsTemplatestring设置当前瀑布流组件布局列的数量,不设置时默认1列。
例如, ‘1fr 1fr 2fr’ 是将父组件分3列,将父组件允许的宽分为4等份,第一列占1份,第二列占1份,第三列占2份。并支持auto-fill。
默认值:‘1fr’
rowsTemplatestring设置当前瀑布流组件布局行的数量,不设置时默认1行。
itemConstraintSizeConstraintSizeOptions设置约束尺寸,子组件布局时,进行尺寸范围限制。
columnsGapLength设置列与列的间距。
默认值:0
rowsGapLength设置行与行的间距。
默认值:0
layoutDirectionFlexDirection设置布局的主轴方向。
默认值:FlexDirection.Column
3、事件
名称功能描述
onReachStart(event: () => void)瀑布流组件到达起始位置时触发。
onReachEnd(event: () => void)瀑布流组件到底末尾位置时触发。

七、使用示例(无限瀑布流)

1、实现代码
// WaterFlowDataSource.ets

// 实现IDataSource接口的对象,用于瀑布流组件加载数据
export class WaterFlowDataSource implements IDataSource {

  private dataArray: number[] = []
  private listeners: DataChangeListener[] = []

  // 获取索引对应的数据
  public getData(index: number): any {
    return this.dataArray[index]
  }

  // 获取数据总数
  public totalCount(): number {
    return this.dataArray.length
  }

  // 注册改变数据的控制器
  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      this.listeners.push(listener)
    }
  }

  // 注销改变数据的控制器
  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener)
    if (pos >= 0) {
      this.listeners.splice(pos, 1)
    }
  }

  // 通知控制器数据增加
  notifyDataAdd(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataAdd(index)
    })
  }

  //增加数据
  public pushData(data: number): void {
    this.dataArray.push(data);
    this.notifyDataAdd(this.dataArray.length - 1);
  }
}

这里面只用到了添加方法,固其他方法省略,只保留基本的方法。

// WaterflowDemo.ets

import { WaterFlowDataSource } from './WaterFlowDataSource'

@Entry
@Component
struct WaterflowDemo {
  @State minSize: number = 80
  @State maxSize: number = 280
  @State colors: number[] = [0xFFC0CB, 0xDA70D6, 0x6B8E23, 0x6A5ACD, 0x00FFFF, 0x00FF7F]
  datasource: WaterFlowDataSource = new WaterFlowDataSource()
  private itemWidthArray: number[] = []
  private itemHeightArray: number[] = []

  // 计算flow item宽/高
  getSize() {
    let ret = Math.floor(Math.random() * this.maxSize)
    return (ret > this.minSize ? ret : this.minSize)
  }

  // 新增数据,并保存flow item宽/高
  getItemSizeArray(t=0) {
    for (let i = 0; i < 20; i++) {
      this.itemWidthArray.push(this.getSize())
      this.itemHeightArray.push(this.getSize())
      this.datasource.pushData(i+t)
    }
  }

  aboutToAppear() {
    this.getItemSizeArray()
  }

  build() {
    Column() {
      WaterFlow() {
        LazyForEach(this.datasource, (item: number) => {
          FlowItem() {
            Column() {
              Text("N" + item).fontSize(14)
            }
          }
          .width(this.itemWidthArray[item])
          .height(this.itemHeightArray[item])
          .backgroundColor(this.colors[item % 5])
          // 此处通过在FlowItem的onAppear中判断距离数据终点的数量,提前增加数据的方式实现了无停顿的无限滚动。
          .onAppear(() => {
            // 即将触底时提前增加数据
            if (item + 20 == this.datasource.totalCount()) {
              this.getItemSizeArray(this.datasource.totalCount())
            }
          })
        }, item => item)
      }
      .columnsTemplate("1fr 1fr")
      .columnsGap(10)
      .rowsGap(10)
      // 瀑布流组件到达起始位置时触发。
      .onReachStart(() => {
        console.info("onReachStart")
      })
      // 瀑布流组件到底末尾位置时触发。
      .onReachEnd(() => {
        console.info("onReachEnd")
      })
      .backgroundColor(0xFAEEE0)
      .width('100%')
      .height('100%')
    }
  }
}

这里没有图片,用了随机背景色代替。

2、实现效果

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

邹荣乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值