鸿蒙-LazyForEach 和 IDataSource封装

531 阅读8分钟

一 概述

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

LazyForEach和ForEach对比

2.1 ForEach循环渲染的过程如下:

  1. 从列表数据源一次性加载全量数据。
  2. 为列表数据的每一个元素都创建对应的组件,并全部挂载在组件树上。即,ForEach遍历多少个列表元素,就创建多少个ListItem组件节点并依次挂载在List组件树根节点上。
  3. 列表内容显示时,只渲染屏幕可视区内的ListItem组件,可视区外的ListItem组件滑动进入屏幕内时,因为已经完成了数据加载和组件创建挂载,直接渲染即可。

2.2 LazyForEach

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

LazyForEach使用

  • 仅有ListGridSwiper以及WaterFlow组件支持数据懒加载(可配置cachedCount属性(只有在LazyForEach有效),即只加载可视部分以及其前后少量数据用于缓冲)
  • LazyForEach在每次迭代中,必须创建且只允许创建一个子组件。
  • 生成的子组件必须是允许包含在LazyForEach父容器组件中的子组件。
  • 允许LazyForEach包含在if/else条件渲染语句中,也允许LazyForEach中出现if/else条件渲染语句。
  • 键值生成器必须针对每个数据生成唯一的值,如果键值相同,将导致键值相同的UI组件渲染出现问题。
  • LazyForEach必须使用DataChangeListener对象来进行更新,第一个参数dataSource使用状态变量时,状态变量改变不会触发LazyForEach的UI刷新。
  • 为了高性能渲染,通过DataChangeListener对象的onDataChange方法来更新UI时,需要生成不同于原来的键值来触发组件刷新。
  • LazyForEach必须和@Reusable装饰器一起使用才能触发节点复用。使用方法:将@Reusable装饰在LazyForEach列表的组件上,见使用规则
  • 键值生成函数keyGenerator中不推荐使用stringify。在复杂的业务场景中,使用stringify会对item对象进行序列化,最终把item转换成字符串,这过程需要消耗大量的时间和计算资源,从而导致页面性能降低

三 LazyForEach 使用方法

必须配合 IDataSource 来使用 和 cachedCount(建议使用) 需要定义两个 BasicDataSource(来存原始数据)和 一个 继承 BasicDataSource 咱们自己的DataSource,

3.0 键值生成规则

如果开发者没有定义 keyGenerato r函数,则ArkUI框架会使用默认的键值生成函数,即(item: Object, index: number) => { return viewId + '-' + index.toString(); }, viewId在编译器转换过程中生成,同一个LazyForEach组件内其viewId是一致的。相当于就是跟index有关,所以我们必须定义 keyGenerato r函数,并且避免,JSON.stringify(item),原因是当数据过大的时候,影响性能

3.1 IDataSource 方法

下面的方法一般是固定写法

  • totalCount:获得数据总数。
  • getData:获取索引值index对应的数据。
  • registerDataChangeListener:注册数据改变的监听器。该方法为框架侧调用,为LazyForEach组件向其数据源处添加listener监听
  • unregisterDataChangeListener:注销数据改变的监听器。该方法为框架侧调用,为LazyForEach组件向其数据源处移除listener监听
export class BasicDataSource<T> implements IDataSource {
  // 固定写法 所有的改变的监听器
  private listeners: DataChangeListener[] = [];
  // 固定写法 原始数据
  private originDataArray: T[] = [];
  // 获得数据总数。
  public totalCount(): number {
    return 0;
  }
  // 获取数据
  public getData(index: number): T {
    return this.originDataArray[index];
  }
  // 固定写法 该方法为框架侧调用,为LazyForEach组件向其数据源处添加listener监听
  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      this.listeners.push(listener);
    }
  }
  // 固定写法 该方法为框架侧调用,为对应的LazyForEach组件在数据源处去除listener监听
  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener);
    if (pos >= 0) {
      console.info('remove listener');
      this.listeners.splice(pos, 1);
    }
  }
}

3.2 DataChangeListener

因为 onDataReloaded 、onDataAdd、onDataMove、onDataDelete、onDataChangelistener.onDatasetChange() 这两个不能同时使用,否则会报错 Error message:onDatasetChange cannot be used with other interface

  • onDataReloaded 建议使用 listener.onDatasetChange([{type: DataOperationType.RELOAD}] 通知组件重新加载所有数据。键值没有变化的数据项会使用原先的子组件,键值发生变化的会重建子组件。重新加载数据完成后调用。
  • onDataAdd 建议使用listener.onDatasetChange([{type:DataOperationType.ADD, index: index}]);通知组件index的位置有数据添加。添加数据完成后调用
  • onDataMove(from: number, to: number):建议使用listener.onDatasetChange([{ type: DataOperationType.MOVE, index: { from: from, to: to } }]; )通知组件数据有移动。将from和to位置的数据进行交换。数据移动起始位置与数据移动目标位置交换完成后调用。
  • onDataDelete:建议使用 listener.onDatasetChange([{type: DataOperationType.DELETE, index: index}]); 通知组件删除index位置的数据并刷新LazyForEach的展示内容。删除数据完成后调用
  • onDataChange(index: number): void 建议使用 listener.onDatasetChange([{type: DataOperationType.CHANGE, index: index}]); 通知组件index的位置有数据有变化。改变数据完成后调用。
  • onDatasetChange 方法 这个比较多

3.2.1 onDatasetChange

onDatasetChange(dataOperations: DataOperation[])
3.2.1.1 DataOperation
  • DataAddOperation:添加数据操作。即 {type: DataOperationType.ADD, index: index}

  • DataDeleteOperation:删除数据操作。即 {type: DataOperationType.DELETE, index: index}

  • DataChangeOperation:改变数据操作。即 {type: DataOperationType.CHANGE, index: index}

  • DataMoveOperation:移动数据操作 即 { type: DataOperationType.MOVE, index: { from: from, to: to } }

  • DataExchangeOperation:交换数据操作。即 {type: DataOperationType.CHANGE, index: index}

  • DataReloadOperation:重载所有数据操作。当onDatasetChange含有DataOperationType.RELOAD操作时,其余操作全部失效,框架会自己调用keygenerator进行键值比对 即 {type: DataOperationType.RELOAD}

封装一个BaseDataSource

onDataReloaded 、onDataAdd、onDataMove、onDataDelete、onDataChangelistener.onDatasetChange() 所以,都是用 onDatasetChange 方式 主要方法有

  • addData(data: T, index?: number),向数组末尾添加一个元素或者是指定位置添加一个元素
  • addAllData(data: Array<T>) 在末尾添加一个数组
  • setNewData(data: Array<T>) 添加全新的数据,也就是清空原有的数据,添加新的数组
  • deleteData(index: number) 删除 index 下的数组,如果 index 大于或者等于 ,则不删除,也不添加
  • moveData(from: number, to: number) 移动数据,如果 from to 大于或者等于 则不移动
  • notifyItemChange(index: number, data: T) 通知单个item更改,如果 大于或者等于 数组的length 就不操作
class BasicDataSource<T> implements IDataSource {
  private listeners: DataChangeListener[] = [];
  private originDataArray: T[] = [];

  public totalCount(): number {
    return 0;
  }

  public getData(index: number): T {
    return this.originDataArray[index];
  }

  // 系统调用,开发不用关心
  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);
    }
  }

  // 通知LazyForEach组件需要重载所有子组件,这个有可能闪烁
  // 用 onDatasetChange 代替onDataReloaded,不仅可以修复闪屏的问题,还能提升加载性能
  // https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-rendering-control-lazyforeach-V5
  // 通知组件重新加载所有数据。键值没有变化的数据项会使用原先的子组件,键值发生变化的会重建子组件。重新加载数据完成后调用。
  notifyDataReload(): void {
    this.listeners.forEach(listener => {
      listener.onDatasetChange([{ type: DataOperationType.RELOAD }]);
    })
  }

  notifyDatasetChange(operations: DataOperation[]): void {
    this.listeners.forEach(listener => {
      listener.onDatasetChange(operations);
    })
  }
}

export class BaseDataSource<T> extends BasicDataSource<T> {
  private dataArray: T[] = [];

  public totalCount(): number {
    return this.dataArray.length;
  }

  /**
   * 获取数据
   * @param index
   * @returns
   */
  public getData(index: number): T {
    return this.dataArray[index];
  }

  private addIndexData(index: number, data: T): void {
    if (this.dataArray.length >= index) {
      this.dataArray.splice(index, 0, data);
      this.notifyDatasetChange([
        { type: DataOperationType.ADD, index: index }])
    } else {
      console.warn(`addData 当前的index=${index} 已经大于 数组的length=${this.dataArray.length}`)
    }
  }

  /**
   * 删除 index 下的数组,如果 index 大于或者等于 ,则不删除,也不添加
   * @param index
   * @returns 是否成功
   */
  public deleteData(index: number): boolean {
    if (this.dataArray.length > index) {
      this.dataArray.splice(index, 1);
      this.notifyDatasetChange([
        { type: DataOperationType.DELETE, index: index }
      ])
      return true;
    }
    console.warn(`deleteData 当前的index=${index} 已经大于或者等于 数组的length=${this.dataArray.length}`)
    return false;
  }

  /**
   * 移动数据,如果 from to 大于或者等于 则不移动
   * @param from 要移动的index
   * @param to 要移动到的目标index
   * @returns 是否成功
   */
  public moveData(from: number, to: number): boolean {
    if (this.dataArray.length > from && this.dataArray.length > to) {
      this.notifyDatasetChange([
        { type: DataOperationType.MOVE, index: { from: from, to: to } }
      ])
      return true
    }
    console.warn(`moveData 当前的from=${from} to=${to} 已经大于或者等于 数组的length=${this.dataArray.length}`)
    return false
  }

  /**
   * 向数组末尾添加一个元素
   * @param arr 要添加元素的数组
   * @param element 要添加的元素
   * @returns 返回添加元素后的数组
   */
  public addData(data: T, index?: number): void {
    if (index) {
      this.addIndexData(index, data);
    } else {
      this.dataArray.push(data);
      this.notifyDatasetChange([
        { type: DataOperationType.ADD, index: this.dataArray.length - 1 }])
    }
  }

  /**
   * 通知单个item更改
   * 如果是新的数据,那么相当于重新设置了数据,如果里面有图片 ,图片会闪一下
   * 可以使用 @Observed@ObjectLink 配合使用去更新数据,避免闪烁
   * @param index
   * @param data
   * @returns 是否成功
   */
  public notifyItemChange(index: number, data: T): boolean {
    // 是否需要这样,这个的本意就是通知当前item改变了
    if (this.dataArray.length > index) {
      // 但是如果没有这个index ,这里会增加
      this.dataArray.splice(index, 1, data);
      this.notifyDatasetChange([
        { type: DataOperationType.CHANGE, index: index }])
      return true;
    }
    console.warn(`notifyItemChange 当前的index=${index} 已经大于或者等于 数组的length=${this.dataArray.length}`)
    return false;
  }

  /**
   * 在末尾添加一个数组
   * @param data
   */
  public addAllData(data: Array<T>): void {
    const totalCount = this.dataArray.length;
    this.dataArray.push(...data);
    this.notifyDatasetChange([
      { type: DataOperationType.ADD, index: totalCount, count: data.length }])
  }


  /**
   * 添加全新的数据,也就是清空原有的数据,添加新的数组
   * @param data
   */
  public setNewData(data: Array<T>) {
    let oldLength = this.dataArray.length
    this.dataArray.splice(0, this.dataArray.length);
    this.dataArray.push(...data)
    this.notifyDatasetChange([
      { type: DataOperationType.DELETE, index: 0, count: oldLength },
      { type: DataOperationType.ADD, index: 0, count: data.length },
      // 这样可以服用以前的数据,比如 setNewData 两次同样的数据,如果不加这个会闪烁
      { type: DataOperationType.RELOAD },

    ])
  }
}

1724234047774.gif

源码地址

已经封装好的 BasicDataSource 实现下拉刷新,上拉加载更多