Harmony Next - LazyForEach 列表懒加载

302 阅读5分钟

在当下移动端设计中,列表页面可以说是每个应用都会有的一个设计页面。对于开发同学来说,掌握如何高性能的开发一个列表页面也是非常必要的技能。

在鸿蒙系统中,如果使用 List 组件去渲染列表页面,默认是一次性将所有列表单元格全部加载渲染出来。比如下面的代码示例:

@Entry
@Component
struct Index {
  private dataSource: string[] = [];

  build() {
    List({ space: 5 }) {
      ForEach(this.dataSource, (item: string) => {
        ListItem() {
          Text(item)
        }
        .width('100%')
        .height(44)
        .backgroundColor(Color.Gray)
      })
    }
    .padding({left: 16, right: 16})
  }

  aboutToAppear(): void {
    for (let index = 0; index < 20; index++) {
      this.dataSource.push(`这是第 ${index} 条数据!`)
    }
  }
}

上述代码创建了一个包含 20 条字符串内容的数组当做 List 组件的数据源。然后使用 ForEach 将每个字符串元素渲染为一个 ListItem 组件将其展示在页面中,效果图如下:

截屏2024-11-18 15.53.58.png

这只是一个简单的示例,但实际上的项目中列表的单元格动辄都是成千上万的数量,而且单元格样式也会复杂的多。如果我们一次性加载出来,那对手机的性能就是一个巨大的消耗,造成 APP 卡顿,给用户带来不好的用户体验。

所以,在实际项目开发中,我们应该使用 LazyForEach 去进行列表的数据懒加载,从而提升项目性能。下面,我们就来看下它是如何使用的吧!

基本使用

首先我们来看一下 LazyForEach 的构造函数: (dataSource: IDataSource, itemGenerator: (item: any, index: number) => void, keyGenerator?: (item: any, index: number) => string),可以看到它需要传递三个参数:

  • dataSource:数据源,需要实现 IDataSource 接口。IDataSource的所有接口如下:
    • totalCount:数据源的总条目。
    • getData(index: number):获取 index 对应的数据。
    • registerDataChangeListener(listener: DataChangeListener): void:注册数据监听器
    • unregisterDataChangeListener(listener: DataChangeListener): void:注销数据监听器
  • itemGenerator:生成列表单元格的箭头函数。
  • keyGenerator:可选参数,生成单元格唯一标识的箭头函数。不传的默认为index + '__' + JSON.stringify(item);。建议传入自定义的箭头函数。

Tips:监听器可以执行数据增删改查的回调,后面再详细介绍。

下面我们就来改造一下上面的例子,使用 LazyForEach 来渲染列表,示例代码如下:

// 数据源实现 IDataSource 接口
class ListDataSource implements IDataSource {
  private listeners: DataChangeListener[] = [];
  private dataSource: string[] = [];

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

  public getData(index: number): string {
    return this.dataSource[index];
  }

  // 该方法为框架侧调用,为LazyForEach组件向其数据源处添加listener监听
  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      console.info('add listener');
      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);
    }
  }
  // 此接口用于数据源数据添加,与 IDataSource 无关
  public pushData(data: string): void {
    this.dataSource.push(data);
  }
}

@Entry
@Component
struct Index {
  private dataSource: ListDataSource = new ListDataSource();

  build() {
    List({ space: 5 }) {
      LazyForEach(this.dataSource, (item: string) => {
        ListItem() {
          Text(item)
        }
        .width('100%')
        .height(44)
        .backgroundColor(Color.Gray)
      })
    }
    .padding({left: 16, right: 16})
  }

  aboutToAppear(): void {
    for (let index = 0; index < 20; index++) {
    // 这里改成xx是因为掘金编译器文字统计对美刀符号有bug。。。。
      this.dataSource.pushData(`这是第 xx 条数据!`)
    }
  }
}

首先,因为 LazyForEach 的第一个参数需要实现 IDataSource 接口的实例对象,所以我们需要自定义一个 ListDataSource 类去实现IDataSource 的所有接口。

接着我们将 ForEach 替换为 LazyForEach,然后将数据源改为 ListDataSource 类型即可实现列表数据的懒加载!

增删改移

当我们使用列表视图时,可能会根据需求对列表进行增加、删除等各种操作,那这些操作的回调我们就可以通过 DataChangeListener 来实现,接下来我们逐一实现看一下。

1、添加元素

// 在 ListDataSource 类中添加以下接口:

// 通知LazyForEach组件需要在index对应索引处添加子组件
notifyDataAdd(index: number): void {
  this.listeners.forEach(listener => {
    listener.onDataAdd(index);
  })
}

//然后将 pushData接口修改如下:
public pushData(data: string) {
  this.dataSource.push(data);
  this.notifyDataAdd(this.dataSource.length - 1);
}

Index 中添加按钮模拟添加元素的逻辑:

Button("Add")
  .onClick(() => {
    this.dataSource.pushData("添加新的元素")
  })

点击按钮,页面的第一条就会显示新添加的内容。

2、删除元素

// 在 ListDataSource 类中添加以下接口:
// 通知LazyForEach组件需要在index对应索引处删除该子组件
notifyDataDelete(index: number): void {
  this.listeners.forEach(listener => {
    listener.onDataDelete(index);
  })
}

public removeData(index: number) {
  this.dataSource.splice(index, 1);
  this.notifyDataDelete(index);
}

Index 中添加按钮模拟删除元素的逻辑:

Button("Delete")
  .onClick(() => {
    this.dataSource.removeData(1);
  })

点击按钮,页面的就会删除第二条数据。

3、修改元素

// 在 ListDataSource 类中添加以下接口:
// 通知LazyForEach组件在index对应索引处数据有变化,需要重建该子组件
notifyDataChange(index: number): void {
  this.listeners.forEach(listener => {
    listener.onDataChange(index);
  })
}


public changeData(index: number, data: string) {
  this.dataSource[index] = data;
  this.notifyDataChange(index);
}

Index 中添加按钮模拟修改元素的逻辑:

Button("Change")
  .onClick(() => {
    this.dataSource.changeData(2, "这是修改的元素")
  })

点击按钮,页面的就会将第三条数据更新为新的内容。

4、移动 当进行拖拽排序时,官方文档说明无需调用 DataChangeListener 的 notifyDataMove 接口。

// 在 ListDataSource 类中添加以下接口:
public moveDataWithoutNotify(from: number, to: number): void {
  let tmp = this.dataSource.splice(from, 1);
  this.dataSource.splice(to, 0, tmp[0])
}

然后修改 Index 中的 LazyForEach

List({ space: 5 }) {
  LazyForEach(this.dataSource, (item: string) => {
    ListItem() {
      Text(item)
    }
    .width('100%')
    .height(44)
    .backgroundColor(Color.Gray)
  }, (item: string) => item)
    .onMove((from:number, to:number)=>{
    this.dataSource.moveDataWithoutNotify(from, to)
  })
}
.padding({left: 16, right: 16})

这里需要注意的是,一定要传自定义的生成 key 的箭头函数,否则不能触发拖拽排序的效果。

注意事项

虽然 LazyForEach 接口能提升性能,但它还是有一些使用限制的:

  • 它仅可在 List、Grid、Swiper、WaterFlow 等容器组件中使用。
  • 不可以和 ForEach、ListItem 混用,且建议容器组件中仅使用一个 LazyForEach
  • LazyForEach必须使用 DataChangeListener 对象进行更新,对 dataSource 重新赋值会异常
  • dataSource使用状态变量时,状态变量改变不会触发LazyForEach的UI刷新,需借助 DataChangeListener 接口对页面进行刷新。