[鸿蒙] --- 使用LazyForEach懒加载数据列表

一  介绍

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

简而言之,其实它和前端的虚拟列表基本一样,只渲染可见区域的数据,来提升页面的响应速度,减少卡顿。

二  使用介绍

接口描述

LazyForEach(
    dataSource: IDataSource,             // 需要进行数据迭代的数据源
    itemGenerator: (item: any, index: number) => void,  // 子组件生成函数
    keyGenerator?: (item: any, index: number) => string // 键值生成函数
): void

LazyForEach是一个函数接口,该函数接收三个参数:dataSource(必填),itemGenerator(必填),keyGenerator(可选)。

以下我也将简单介绍这三个参数的使用。

dataSource

类型为 IDataSource 的数据源,需要开发者实现相关接口。该参数提供需要进行遍历和处理的数据集合。而我们要了解数据源如何定义,就不得不提及类型IDataSource和数据变化监听器DataChangeListener。

IDataSource类型说明

interface IDataSource {
    totalCount(): number; // 获得数据总数
    getData(index: number): Object; // 获取索引值对应的数据
    registerDataChangeListener(listener: DataChangeListener): void; // 注册数据改变的监听器
    unregisterDataChangeListener(listener: DataChangeListener): void; // 注销数据改变的监听器
}

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; // 改变数据完成后调用
}

itemGenerator

子组件生成函数,为数组中的每一个数据项创建一个子组件。其参数为当前数据项 item(类型为 any)和该数据项的索引值 index(类型为 number)。

此函数的目的是为每个数据项生成一个子组件。函数体必须使用大括号 {...} 包裹,且每次迭代必须生成一个子组件。在 itemGenerator 中,可以使用条件语句(如 if),但必须确保每个条件分支都会创建。

keyGenerator

键值生成函数,用于为数据源中的每个数据项生成唯一且固定的键值。其参数同样为当前数据项 item 和索引值 index。数据源中的每一个数据项生成的键值不能重复。

当数据项在数组中的位置更改时,其键值不得更改,当数组中的数据项被新项替换时,被替换项的键值和新项的键值必须不同。键值生成器的功能是可选的,但是,为了使开发框架能够更好地识别数组更改,提高性能,建议提供。如将数组反向时,如果没有提供键值生成器,则LazyForEach中的所有节点都将重建。

本参数虽然为可选参数,但如果你不做设置的话,当LazyForEach数据源发生变化,需要再次渲染时,LazyForEach渲染的数据项键值会均相同。在滑动过程中,LazyForEach会对划入划出当前页面的子组件进行预加载,而新建的子组件和销毁的原子组件具有相同的键值,框架可能存在取用缓存错误的情况,导致子组件渲染有问题。

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

键值生成规则

在LazyForEach循环渲染过程中,系统会为每个item生成一个唯一且持久的键值,用于标识对应的组件。当这个键值变化时,ArkUI框架将视为该数组元素已被替换或修改,并会基于新的键值创建一个新的组件。

LazyForEach提供了一个名为keyGenerator的参数,这是一个函数,开发者可以通过它自定义键值的生成规则。如果开发者没有定义keyGenerator函数,则ArkUI框架会使用默认的键值生成函数,即

(item: any, index: number) => { 
  return viewId + '-' + index.toString();
 },

viewId在编译器转换过程中生成,同一个LazyForEach组件内其viewId是一致的。

使用限制

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

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

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

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

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

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

三  方法实现

封装泛型类

在BasicDataSource类的实现中,泛型T被用作originDataArray的类型参数,确保存储的数据元素与期望的类型一致,同时getData方法也返回类型T的元素,保证了类型的一致性。

// Basic implementation of IDataSource to handle data listener
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) {
      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);
    }
  }

  // 通知LazyForEach组件需要重载所有子组件
  notifyDataReload(): void {
    this.listeners.forEach(listener => {
      listener.onDataReloaded();
    })
  }

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

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

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

  // 通知LazyForEach组件将from索引和to索引处的子组件进行交换
  notifyDataMove(from: number, to: number): void {
    this.listeners.forEach(listener => {
      listener.onDataMove(from, to);
    })
  }
}

ListDataSource是继承自BasicDataSource的泛型类,用于管理特定类型的数据列表。它具有totalCount()获取数据总数,getData()按索引获取数据,addData()在指定位置插入数据并通知监听器,以及pushData()在列表末尾添加数据并通知的功能。

// 基础类
export class ListDataSource<T> extends BasicDataSource<T> {
  private dataArray: T[] = [];

  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);
  }
}

 以及为了满足下拉刷新的业务需求,自定义了一个reloadData方法,用于重新加载数据。此方法会将数组dataArray重置为空数组,再调用notifyDataReload函数,通知所有注册的数据监听器进行数据更新操作。便于之后LazyForEach重新自动渲染数据。

  public reloadData () {
    this.dataArray = [] // 不会引起状态变化
    // 必须通过DataChangeListener来更新
    this.notifyDataReload() // 重新加载数据
  }

注:当这些基本方法不能满足我们的需求时,可以自定义方法来实现,记得调用notifyDataReload()  重新加载数据。

数据渲染

这是基于官方案例的简单展现,代码如下。

import { ListDataSource } from '../models';

class sun {
   name: string = 'hello'
   sum: number = 0

   constructor(name: string, sum: number) {
      this.name = name;
      this.sum = sum;
   }
}
@Entry
@Component
struct MyComponent {
   @State data: ListDataSource<sun> = new ListDataSource();

   aboutToAppear() {
      for (let i = 0; i <= 100; i++) {
         this.data.pushData(new sun('hello',i))
      }
   }

   build() {
      List({ space: 3 }) {
         LazyForEach(this.data, (item: sun) => {
            ListItem() {
               Row({space:10}) {
                  Text(item.name ).fontSize(50)
                  Text(item.sum.toString()).fontSize(50)
               }.margin({ left: 10, right: 10 })
            }
         }, (item: sun) => item.sum.toString())
      }.cachedCount(5)
   }
}

这是真实场景中的部分代码。

import { getArticleList } from '../../api'
import { ArticleItem, ListDataSource } from '../../models'
import { promptAction } from '@kit.ArkUI'

@Component
struct Home {
  @State
  refresh: boolean = false
  @State
  list: ListDataSource<ArticleItem> = new ListDataSource<ArticleItem>()
  @State
  loading: boolean = false
  @State
  pre_timestamp: number = 0
  @State
  refreshStatus: RefreshStatus = RefreshStatus.Inactive
  @State
  finished: boolean = false // 是否已经加载完成

  list1: ListDataSource<ArticleItem> = new ListDataSource<ArticleItem>()
  
  async getArticleList() {
    const result = await getArticleList({ channel_id: 0, timestamp: this.pre_timestamp || Date.now() })
    this.pre_timestamp = result.pre_timestamp
    result.results.forEach(item => {
      this.list.pushData(item)
    })
    this.finished = !this.pre_timestamp
  }

  async reload() {
    this.finished = false
    const result = await getArticleList({ channel_id: 0, timestamp: Date.now() })
    this.pre_timestamp = result.pre_timestamp
    this.list.reloadData() // 清空数据
    result.results.forEach(item => {
      this.list.pushData(item)
    })
  }
  
  @Builder
  getArticleItem(item: ArticleItem) {
    Column({ space: 10 }) {
      if (item.cover.type === 1) {
        Row() {
          Text(item.title)
            .fontSize(16)
          Image(item.cover.images[0])
            .width(100)
            .height(60)
            .borderRadius(4)
        }
        .justifyContent(FlexAlign.SpaceBetween)
        .width("100%")
        .alignItems(VerticalAlign.Top)
      } else if (item.cover.type === 3) {
        Column({ space: 10 }) {
          Text(item.title)
            .fontSize(16)
          Row({ space: 10 }) {
            ForEach(item.cover.images, (url: string, index: number) => {
              Image(url)
                .layoutWeight(1)
                .height(60)
                .borderRadius(4)
            })
          }
          .width("100%")
        }
        .width("100%")
        .alignItems(HorizontalAlign.Start)
      } else {
        Text(item.title)
          .fontSize(16)
          .width("100%")
      }
      Row({ space: 10 }) {
        Text(item.aut_name)
          .fontColor(Color.Gray)
          .fontSize(12)
        Text(`${item.comm_count}评论`)
          .fontColor(Color.Gray)
          .fontSize(12)
        Text(item.pub_date)
          .fontColor(Color.Gray)
          .fontSize(12)
      }
      .width("100%")
    }
    .width("100%")
    .padding(20)
    .border({
      color: Color.Gray,
      width: {
        bottom: 0.5
      }
    })
  }

  getTextByStatus() {
    switch (this.refreshStatus) {
      case RefreshStatus.Drag:
        return "继续下拉"
      case RefreshStatus.OverDrag:
        return "松手加载"
      case RefreshStatus.Refresh:
        return "加载中"
    }
    return ""
  }

  @Builder
  getRefreshBuilder() {
    Row({ space: 20 }) {
      Text(this.getTextByStatus())
        .fontSize(12)
        .fontColor(Color.Red)
      LoadingProgress()
        .width(20)
        .height(20)
        .color(Color.Red)
    }
    .height(50)
    .justifyContent(FlexAlign.Center)
    .width("100%")
  }

  build() {
    // 下拉刷新 Refresh
    // 上拉加载 List
    Refresh({ refreshing: $$this.refresh, builder: this.getRefreshBuilder }) {
      List() {
        LazyForEach(this.list, (item: ArticleItem) => {
          ListItem() {
            this.getArticleItem(item)
          }
        }, (item: ArticleItem) => item.art_id)
        if (this.finished) {
          ListItem() {
            Row() {
              Text("没啦")
                .fontSize(12)
            }
            .width("100%")
            .height(40)
            .justifyContent(FlexAlign.Center)
          }
        } else {
          if (this.loading) {
            // 正在加载中
            ListItem() {
              Row({ space: 10 }) {
                Text("加载数据中")
                  .fontSize(12)
                LoadingProgress()
                  .width(20)
                  .height(20)
              }
              .width("100%")
              .height(40)
              .justifyContent(FlexAlign.Center)
            }
          }
        }
      }
      .onReachEnd(async () => {
        // 阀门控制
        if (!this.loading && !this.finished) {
          this.loading = true
          await this.getArticleList()
          this.loading = false
        }

      })
    }
    .onStateChange(async (status) => {
      this.refreshStatus = status
      if (this.refreshStatus === RefreshStatus.Refresh) {
        await this.reload()
        this.refresh = false // 手动关闭状态
        promptAction.showToast({ message: '刷新成功' })
      }
    })
    // .onRefreshing() // 只会在超过一定区域 松手时触发
  }
}

export default Home
 

四  结语

 因为我们使用LazyForEach也主要是用于大量列表数据的渲染的场景,是为了解决使用ForEach循环数据列表中长期下拉添加数据导致的页面卡顿,不流畅问题,同时实现项目性能优化,所以本文主要介绍的就是LazyForEach的循环数据源,和自定义清空实现下拉刷新。

所以添加,删除,交换,改变数据等操作的实际使用情景,请参考文档自己实现。(抱歉...)

LazyForEach:数据懒加载 (openharmony.cn)

  • 37
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值