鸿蒙开发-LazyForEach你的懒让我忙死了

1,372 阅读5分钟

为什么要使用懒加载-LazyForEach

我们在使用ForEach进行循环遍历的时候,框架会加载所有的数据源,这样就会导致如果遍历内容太多,占据大量的内存占用,进而导致性能降低

LazyForEach从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。 当在滚动容器中使用了LazyForEach,框架会根据滚动容器可视区域按需创建组件,当组件滑出可视区域外时,框架会进行组件销毁回收降低内存占用

使用过程

官网方式

1 实现提供的一个 IDataSource 的接口

ArkTS类BasicDataSource实现了数据源功能,主要用于配合LazyForEach组件使用,支持数据监听和通知机制:

存储监听器和数据:通过listeners数组存储数据变化监听器,originDataArray存储原始数据。

获取数据总数:totalCount()方法固定返回0,表示不支持动态总数展示。

获取指定索引数据:getData(index)方法根据索引返回数据项。

监听器管理:提供registerDataChangeListener和unregisterDataChangeListener方法来添加或移除监听器。

数据变化通知:通过notifyDataReload、notifyDataAdd、notifyDataChange、notifyDataDelete和notifyDataMove方法通知监听器进行数据重载、添加、修改、删除及移动操作。

/**
 * BasicDataSource类是一个数据源的实现,用于配合LazyForEach组件使用。
 * 它提供了数据监听器的注册与注销功能,并能通知LazyForEach组件数据变化,
 * 以便组件能够相应地更新自身。
 */
export class BasicDataSource implements IDataSource {
  // 存储数据变化监听器的数组
  private listeners: DataChangeListener[] = [];
  // 存储原始数据的数组
  private originDataArray: string[] = [];

  /**
   * 返回数据总数。
   * 目前总是返回0,表示该数据源不支持动态数据总数的展示。
   * @returns number 总数据数,目前固定为0。
   */
  public totalCount(): number {
    return 0;
  }

  /**
   * 根据索引获取数据。
   * @param index 数据在数组中的索引。
   * @returns string 索引对应的数据项。
   */
  public getData(index: number): string {
    return this.originDataArray[index];
  }

  /**
   * 注册数据变化监听器。
   * 框架侧调用此方法向数据源添加监听器,以监听数据变化。
   * @param listener DataChangeListener类型的监听器。
   */
  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      console.info('add listener');
      this.listeners.push(listener);
    }
  }

  /**
   * 注销数据变化监听器。
   * 框架侧调用此方法从数据源处去除监听器。
   * @param listener DataChangeListener类型的监听器。
   */
  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener);
    if (pos >= 0) {
      console.info('remove listener');
      this.listeners.splice(pos, 1);
    }
  }

  /**
   * 通知所有监听器数据需要重新加载。
   * 此方法告知所有注册的监听器,数据源中的所有数据已变更,需要重新加载所有子组件。
   */
  notifyDataReload(): void {
    this.listeners.forEach(listener => {
      listener.onDataReloaded();
    })
  }

  /**
   * 通知监听器在特定索引位置添加数据。
   * @param index 添加数据的索引位置。
   */
  notifyDataAdd(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataAdd(index);
    })
  }

  /**
   * 通知监听器数据在特定索引位置已变更。
   * @param index 数据变更的索引位置。
   */
  notifyDataChange(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataChange(index);
    })
  }

  /**
   * 通知监听器在特定索引位置删除数据。
   * @param index 删除数据的索引位置。
   */
  notifyDataDelete(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataDelete(index);
    })
  }

  /**
   * 通知监听器在特定索引位置移动数据。
   * @param from 移动数据的起始索引位置。
   * @param to 移动数据的目标索引位置。
   */
  notifyDataMove(from: number, to: number): void {
    this.listeners.forEach(listener => {
      listener.onDataMove(from, to);
    })
  }
}

2 将数据包装到对象中,实现一系列增删改查的方法

该类 MyDataSource 继承自 BasicDataSource,用于管理字符串数据数组,实现以下功能:

统计数组长度:totalCount() 方法返回数组中元素的数量。

按索引获取数据:getData(index) 通过索引值获取对应位置的数据。

插入数据并通知:addData(index, data) 在指定索引位置插入数据,并通知数据变化。

追加数据并通知:pushData(data) 在数组末尾添加数据,并通知数据变化。

刷新数据源:reloadData(data) 更新内部数据数组,并通知数据重新加载,常用于界面重新渲染。

import { BasicDataSource } from './BasicDataSource';

/**
 * MyDataSource 类继承自 BasicDataSource,用于管理字符串数据数组
 * 它实现了数据的添加、推送以及通过索引获取数据的功能
 */
export class MyDataSource extends BasicDataSource {
  // 数据数组,用于存储字符串数据
  private dataArray: string[] = [];

  /**
   * 获取数据总条数
   *
   * @returns 数据数组中的元素数量
   */
  public totalCount(): number {
    return this.dataArray.length;
  }

  /**
   * 根据索引获取数据
   *
   * @param index 索引值,用于指定要获取的数据位置
   * @returns 索引位置的数据
   */
  public getData(index: number): string {
    return this.dataArray[index];
  }

  /**
   * 在指定索引位置添加数据,并通知数据已添加
   *
   * @param index 要添加数据的索引位置
   * @param data 要添加的数据
   */
  public addData(index: number, data: string): void {
    this.dataArray.splice(index, 0, data);
    this.notifyDataAdd(index);
  }

  /**
   * 在数组末尾添加数据,并通知数据已添加
   *
   * @param data 要添加的数据
   */
  public pushData(data: string): void {
    this.dataArray.push(data);
    this.notifyDataAdd(this.dataArray.length - 1);
  }
}

3 实现效果

import { MyDataSource } from './MyDataSource';

/**
 * Index组件的主入口,负责数据源的初始化和组件的构建
 */
@Entry
@Component
struct Index {
  // 初始化数据源为MyDataSource实例
  data: MyDataSource = new MyDataSource()

  /**
   * 在组件即将出现时调用,用于加载数据
   */
  aboutToAppear(): void {
    // 向数据源中添加三条数据
    this.data.pushData('app.media.1')
    this.data.pushData('app.media.2')
    this.data.pushData('app.media.3')
  }

  /**
   * 构建组件的UI
   * 使用Column布局,并在其中使用Swiper组件结合LazyForEach实现轮播效果
   */
  build() {
    Column() {
      Swiper() {
        // 使用LazyForEach结合数据源实现性能优化的循环渲染
        LazyForEach(this.data, (item: string) => {
          Image($r(item)) // 对每个数据项创建Image组件
        }, (item: string) => item) // 使用item作为key优化渲染性能
      }
      .width('100%') // 设置Swiper宽度
      .autoPlay(true) // 开启自动播放
    }
    .width('100%') // 设置Column宽度
    .height('100%') // 设置Column高度
  }
}

不推荐官网方式的原因

  1. 数据类型写死,无法实现复用
  2. 步骤过于繁琐,内容太多 image.png

推荐方法

我们竟然无法定义为泛型实现复用,但又不想写这么多

我们为什么不定义一个类,实现IDataSource 接口

image.png


export class MyDataSource implements IDataSource {
  // 数据数组,用于存储字符串数据
  dataArray: string[] = [];
  // 存储数据变化监听器的数组
  listeners: DataChangeListener[] = [];

  /**
   * 获取数据总条数
   *
   * @returns 数据数组中的元素数量
   */
  public totalCount(): number {
    return this.dataArray.length;
  }

  /**
   * 根据索引获取数据
   *
   * @param index 索引值,用于指定要获取的数据位置
   * @returns 索引位置的数据
   */
  public getData(index: number): string {
    return this.dataArray[index];
  }

  /**
   * 在指定索引位置添加数据,并通知数据已添加
   *
   * @param index 要添加数据的索引位置
   * @param data 要添加的数据
   */
  public addData(index: number, data: string): void {
    this.dataArray.splice(index, 0, data);
    this.notifyDataAdd(index);
  }

  /**
   * 在数组末尾添加数据,并通知数据已添加
   *
   * @param data 要添加的数据
   */
  public pushData(data: string): void {
    this.dataArray.push(data);
    this.notifyDataAdd(this.dataArray.length - 1);
  }

  /**
   * 注册数据变化监听器。
   * 框架侧调用此方法向数据源添加监听器,以监听数据变化。
   * @param listener DataChangeListener类型的监听器。
   */
  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      console.info('add listener');
      this.listeners.push(listener);
    }
  }

  /**
   * 注销数据变化监听器。
   * 框架侧调用此方法从数据源处去除监听器。
   * @param listener DataChangeListener类型的监听器。
   */
  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener);
    if (pos >= 0) {
      console.info('remove listener');
      this.listeners.splice(pos, 1);
    }
  }

  /**
   * 通知所有监听器数据需要重新加载。
   * 此方法告知所有注册的监听器,数据源中的所有数据已变更,需要重新加载所有子组件。
   */
  notifyDataReload(): void {
    this.listeners.forEach(listener => {
      listener.onDataReloaded();
    })
  }

  /**
   * 通知监听器在特定索引位置添加数据。
   * @param index 添加数据的索引位置。
   */
  notifyDataAdd(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataAdd(index);
    })
  }

  /**
   * 通知监听器数据在特定索引位置已变更。
   * @param index 数据变更的索引位置。
   */
  notifyDataChange(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataChange(index);
    })
  }

  /**
   * 通知监听器在特定索引位置删除数据。
   * @param index 删除数据的索引位置。
   */
  notifyDataDelete(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataDelete(index);
    })
  }

  /**
   * 通知监听器在特定索引位置移动数据。
   * @param from 移动数据的起始索引位置。
   * @param to 移动数据的目标索引位置。
   */
  notifyDataMove(from: number, to: number): void {
    this.listeners.forEach(listener => {
      listener.onDataMove(from, to);
    })
  }


}

实现封装

封装数据源 实现 IDataSource接口

export class MyDataSource<T> implements IDataSource {
  // 数据数组,用于存储字符串数据
  dataArray: T[] = [];
  // 存储数据变化监听器的数组
  listeners: DataChangeListener[] = [];

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

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

  public addData(index: number, data: T): void {
    this.dataArray.splice(index, 0, data);
    this.notifyDataAdd(index);
  }

  public pushData(data: T): void {
    this.dataArray.push(data);
    this.notifyDataAdd(this.dataArray.length - 1);
  }

  // 注册数据变化
  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      console.info('add listener');
      this.listeners.push(listener);
    }
  }

  // 注销数据变化
  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener);
    if (pos >= 0) {
      console.info('remove listener');
      this.listeners.splice(pos, 1);
    }
  }

  // 通知所有监听器数据需要重新加载
  notifyDataReload(): void {
    this.listeners.forEach(listener => {
      listener.onDataReloaded();
    })
  }

  // 通知监听器在特定索引位置添加数据
  notifyDataAdd(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataAdd(index);
    })
  }

  // 通知监听器数据在特定索引位置已变更
  notifyDataChange(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataChange(index);
    })
  }

  // 通知监听器在特定索引位置删除数据
  notifyDataDelete(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataDelete(index);
    })
  }

  // 通知监听器在特定索引位置移动数据
  notifyDataMove(from: number, to: number): void {
    this.listeners.forEach(listener => {
      listener.onDataMove(from, to);
    })
  }
}

实现

import { MyDataSource } from './MyDataSource';

/**
 * Index组件的主入口,负责数据源的初始化和组件的构建
 */
@Entry
@Component
struct Index {
  // 初始化数据源为MyDataSource实例
  data: MyDataSource<string> = new MyDataSource()

  /**
   * 在组件即将出现时调用,用于加载数据
   */
  aboutToAppear(): void {
    // 向数据源中添加三条数据
    this.data.pushData('app.media.1')
    this.data.pushData('app.media.2')
    this.data.pushData('app.media.3')
  }

  /**
   * 构建组件的UI
   * 使用Column布局,并在其中使用Swiper组件结合LazyForEach实现轮播效果
   */
  build() {
    Column() {
      Swiper() {
        // 使用LazyForEach结合数据源实现性能优化的循环渲染
        LazyForEach(this.data, (item: string) => {
          Image($r(item)) // 对每个数据项创建Image组件
        }, (item: string) => item) // 使用item作为key优化渲染性能
      }
      .width('100%') // 设置Swiper宽度
      .autoPlay(true) // 开启自动播放
    }
    .width('100%') // 设置Column宽度
    .height('100%') // 设置Column高度
  }
}

总结

LazyForEach从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。当在滚动容器中使用了LazyForEach,框架会根据滚动容器可视区域按需创建组件,当组件滑出可视区域外时,框架会进行组件销毁回收以降低内存占用。