重生之我是懒洋洋,我在想等会再干——LazyForEach:数据懒加载

一:LazyForEach 的概念及作用

        LazyForEach 是一种数据渲染策略,它允许开发者按需迭代数据源中的数据,并在每次迭代过程中创建相应的组件。这种方法通常用于滚动容器中,以便只渲染用户当前可见的数据项,从而优化性能和内存使用。当用户滚动时,LazyForEach 会动态加载和卸载数据项,以提供流畅的用户体验并减少不必要的计算。


二:LazyForEach 的工作原理及详解

        LazyForEach 依赖于数据源(IDataSource)子组件生成函数(itemGenerator)。数据源提供了需要迭代的数据集合,而子组件生成函数负责为数据源中的每个数据项创建相应的组件。此外,LazyForEach 还可以配置键值生成函数(keyGenerator),用于为数据项生成唯一的键值,这有助于框架在数据更新时准确地识别哪些组件需要被重新渲染。

        相应的接口模式如下:

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

        隐墨同学乍然一看,这不和 ForEach 循环渲染的接口模式如此的相似,不禁提问 LazyForEach懒加载 和 ForEach 循环渲染 到底有何不同呢?


1.  IDataSource类型

        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; // 改变数据完成后调用
    onDatasetChange(dataOperations: DataOperation[]): void; // 批量数据处理后调用
}

        隐墨同学一看好多好多的数据,这些到底都是什么啊啊啊,她现在感觉好晕晕晕晕,不要害怕,就是类似于创建增删改,同时也是 LazyForEach 的精华所在。


        2.  键值生成规则

        在LazyForEach循环渲染过程中,系统会为每个item生成一个唯一且持久的键值,用于标识对应的组件。当这个键值变化时,ArkUI框架将视为该数组元素已被替换或修改,并会基于新的键值创建一个新的组件。LazyForEach提供了一个名为keyGenerator的参数,如果开发者没有定义keyGenerator函数,则ArkUI框架会使用默认的键值生成函数。(此处和 ForEach 还是相似的)


        3.  组件渲染

 隐墨笔记强调!!!!!!

        由上述的的对于每一个item生成一个唯一且持久的键值,所以对于键值发生变化时就会引起相应的组件展示错误,如果键值相同时错误渲染,新建的子组件和销毁的原子组件具有相同的键值,框架可能存在取用缓存错误的情况,导致子组件渲染有问题。

        基于上边的强调,组件渲染就分为了 首次渲染非首次渲染

        对于定义类来实现IDataSource接口方法(官网上有详细资料)

export class BasicDataSource implements IDataSource {
  private listeners: DataChangeListener[] = [];
  private originDataArray: string[] = [];

  public totalCount(): number {
    return 0;
  }

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

  //indexOf 方法用于返回一个指定值在数组或字符串中第一次出现的索引位置。
  //如果在指定的数组或字符串中没有找到该值,则返回 -1。indexOf 方法是区分大小写的。
  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);   //通知所有监听器数据集在特定位置 index 添加了新的数据项。
    })
  }

  notifyDataChange(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataChange(index);   //表示数据集中的数据项在指定位置发生了改变。
    })
  }

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

        基于这样的类,我们需要定义我们真实处理数据的类,主要用来做我们想做的事情,例如添加元素、删除元素、全部更新 ,简单的举例:

import { BasicDataSource } from '../model/BasicDataSource'


class MyDataSource extends BasicDataSource {
  //列表数据源
  private dataArray: string[] = [];
  
  //数据数量
  public totalCount(): number {
    return this.dataArray.length;
  }

  //获取某条数据
  public getData(index: number): string {
    return this.dataArray[index];
  }

  //指定位置添加一条数据
  public addData(index: number, data: string): void {
    this.dataArray.splice(index, 0, data);
    this.notifyDataAdd(index);
  }

  //结尾添加一条数据
  public pushData(data: string): void {
    this.dataArray.push(data);
    this.notifyDataAdd(this.dataArray.length - 1);
  }
}

                 3.1 首次渲染

        

@Entry
@Component
struct MyComponent {
  private data: MyDataSource = new MyDataSource();

  //执行其build函数之前完成数据的加载
  aboutToAppear() {
    for (let i = 100; i <= 150; i++) {
      this.data.pushData(`我许隐墨有 ${i} 钱`)
    }
  }

  build() {
    List({ space: 3 }) {
      LazyForEach(this.data, (item: string) => {
        ListItem() {
          Row() {
            Text(item).fontSize(30)
              .onAppear(() => {  // 当组件即将显示时执行的回调
                console.info("appear:" + item)
              })
          }.margin({ left: 10, right: 10 })
        }
      }, (item: string) => item)    LazyForEach 的 key 提供函数,通常用于优化渲染性能
    }
    .cachedCount(10)
    .width('100%')
    .height('100%')
  }
}

        简单的实现一个一个首次渲染的懒加载,实际构建动态列表的 ArkTS 组件

        cachedCount(10):设置 List组件的预加载项数,即在滚动列表时,最多预加载10个不在当前视图内的列表项,以减少重新渲染的次数,提高滚动流畅度。(具体可以查询APL,也可以通过测试,不断提高 cachedCount 的数值,就会发现页面滑动的流畅度明显的提高,cachedCount的增加也会增大CPU和内存的开销,因此在使用时需要根据实际情况和性能要求进行调整

        注意事项

        隐墨好高兴哇塞,越往下滑,她的钱就越多,她就可以可以出去好好玩啦,但是兴奋之余,她激动的改了一些代码,但是钱就被无缘无故的没了呜呜呜,她急忙让大家给她看看(节选)

build() {
    List({ space: 3 }) {
      LazyForEach(this.data, (item: string) => {
        ListItem() {
          Row() {
            Text(item).fontSize(30)
              .onAppear(() => {  // 当组件即将显示时执行的回调
                console.info("appear:" + item)
              })
          }.margin({ left: 10, right: 10 })
        }
      }, (item: string) => 'same')    LazyForEach 的 key 提供函数,通常用于优化渲染性能
    }
    .cachedCount(10)
    .width('100%')
    .height('100%')
  }


         出现了问题:当她下滑后再上滑看之前被隐藏的数据,发现他们变了呜呜

        隐墨的父亲一眼就看出来BUG, 代码键值相同导致的问题,便细心的给女儿讲解

        当所有列表项的 key 值相同时,框架将无法区分不同的列表项。这将导致以下问题:

  1. 性能问题:框架在数据更新时,会认为所有列表项都是同一个元素,因此它可能不会重用已渲染的 DOM 节点,而是重新创建和渲染所有项,这将导致性能下降,尤其是在数据量大时,列表滚动或数据更新时的性能损耗会更加明显。

  2. 渲染问题:由于无法区分不同的列表项,框架可能无法正确地更新或重用 DOM 节点,导致列表项的显示状态可能与数据状态不一致,比如列表项的样式、状态或位置可能不正确。

        隐墨同学终于修改好了代码,看着她的钱每次都在不断的刷新,她嘿嘿嘿的笑了!!

                3.2  非首次渲染

        当LazyForEach数据源发生变化,需要再次渲染时,开发者应根据数据源的变化情况调用listener对应的接口,通知LazyForEach做相应的更新(此处只举例增加数据)

build() {
    List({ space: 3 }) {
      LazyForEach(this.data, (item: string) => {
        ListItem() {
          Row() {
            Text(item).fontSize(20)
              .onAppear(() => {
                console.info("appear:" + item)
              })
          }.margin({ left: 10, right: 10 })
        }
        .onClick(() => {
          // 点击追加子组件
          this.data.pushData(`不要做白日梦了,快去实事求是`);
        })
      }, (item: string) => item)
    }.cachedCount(5)
  }

            当不断的点击组件时,首先调用数据源data的pushData方法,该方法会在数据源末尾添加数据并调用notifyDataAdd方法。在notifyDataAdd方法内会又调用listener.onDataAdd方法,该方法会通知LazyForEach在该处有数据添加,LazyForEach便会在该索引处新建子组件。 

        4.   使用限制

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

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

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

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

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

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

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


        三:  ForEach 的不同处

        图片出处为作者为普通网友的《OpenHarmony实战开发-应用列表场景性能提升实践。》

        3.1 ForEach循环渲染的过程:

        

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

        3.2 LazyForEach懒加载的过程:

         LazyForEach实现了按需加载,针对列表数据量大、列表组件复杂的场景,减少了页面首次启动时一次性加载数据的时间消耗,减少了内存峰值。可以显著提升页面的能效比和用户体验。

            3.3 总结

         LazyForEach 和 ForEach 的主要区别在于它们对数据的处理方式和渲染策略。LazyForEach 更适合处理动态数据和大数据集,而 ForEach 更适合处理静态数据或小数据集。在实际应用中,开发者应该根据数据的特性和性能需求选择合适的迭代方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值