关于性能优化——Lazyforeach

一,使用场景

在编程中通常没有 "Lazyforeach" 这样一个标准的、广泛使用的特定概念或工具。不过,从名字推测可能是指类似惰性求值的 foreach 操作或者自定义的具有类似特性的方法等。以下是一些可能的使用场景:

  • 性能优化场景

    • 大数据量处理:当处理大规模数据集时,如果使用传统的 foreach 立即对每个元素进行操作,可能会导致性能问题,尤其是在元素数量巨大且操作较为复杂的情况下。而使用惰性的 foreach,只有在真正需要结果的时候才会进行计算,这样可以避免不必要的计算,提高整体性能。例如在处理海量日志数据时,可能只需要在特定条件下才对日志进行详细分析,使用惰性 foreach 可以在需要时才进行分析操作,而不是一开始就对所有日志进行处理。
    • 按需加载数据:在某些情况下,数据可能是按需加载的,例如从数据库中分页获取数据。惰性 foreach 可以与这种按需加载机制相结合,只有在需要访问下一页数据时才进行实际的数据库查询和数据处理,而不是一次性将所有数据都加载到内存中进行处理,从而节省内存和提高效率。
  • 延迟执行场景

    • 构建复杂操作链:在构建一系列数据处理操作链时,可能希望先定义好对集合中每个元素要执行的操作,但不立即执行,而是等到所有操作都定义好后再一次性执行。惰性 foreach 可以用于这种场景,先将所有操作添加到惰性操作链中,最后再根据需要触发实际的执行,这样可以更灵活地组合和管理数据处理流程。
    • 事件驱动编程:在事件驱动的应用程序中,可能需要在某个事件发生后才对集合中的元素进行操作。使用惰性 foreach 可以将对集合元素的操作定义好,然后在事件触发时才真正执行这些操作,实现事件与数据处理的解耦。
  • 无限序列场景

    • 生成无限序列:在处理一些理论上可以无限生成的序列时,如斐波那契数列等,如果使用传统的 foreach 可能会导致程序陷入无限循环。而惰性 foreach 可以按照需要逐步生成和处理序列中的元素,每次只在需要时计算下一个元素,而不是试图一次性生成整个无限序列,从而避免程序出现问题。
    • 实时数据处理:对于实时产生的数据流,如传感器不断发送的数据,惰性 foreach 可以在数据到达时按需进行处理,而不是在数据还未到达时就进行不必要的等待或预先分配大量资源来处理可能还不存在的数据。

二,Lazyforeach和foreach取舍

1.数据源类型:ForEach:直接接受一个数组作为数据源。LazyForEach:接受一个实现了IDataSource接口

的对象作为数据源。

2.渲染策略:ForEach:一次性渲染所有数据项,适用于数据量较少的情况。LazyForEach:按需渲染数据项,

只渲染可视区域内的数据项,适用于数据量较大的情况,提升性能。

3.内存使用:ForEach:会一次性加载所有的数据项,内存使用较高。LazyForEach:根据可视区域按需加载数

据项,并回收滑出可视区域的数据项,内存使用较低。

4.组件复用:ForEach:没有内置的组件复用机制。LazyForEach:类似于原生开发中的cell复用机制,滑出可

视区域的组件会被回收,新出现的组件优先使用复用池中的组件。

5.性能优化:ForEach:适用于数据量较少且性能要求不高的场景。LazyForEach:适用于数据量较大且性能要

求较高的场景,显著提升页面性能和用户体验。

6.数据监听:ForEach:没有专门的数据监听机制,依赖于数组变化触发的UI 更新。LazyForEach:需要通过DataChangeListener来监听数据变化,手动通知数据变动以刷新UI。

三,layforeach的使用限制

  • LazyForEach必须在容器组件内使用,仅有ListGridSwiper以及WaterFlow组件支持数据懒加载(可配置cachedCount属性,即只加载可视部分以及其前后少量数据用于缓冲),其他组件仍然是一次性加载所有的数据。
  • 容器组件内使用LazyForEach的时候,只能包含一个LazyForEach。以List为例,同时包含ListItem、ForEach、LazyForEach的情形是不推荐的;同时包含多个LazyForEach也是不推荐的。
  • LazyForEach在每次迭代中,必须创建且只允许创建一个子组件;即LazyForEach的子组件生成函数有且只有一个根组件。
  • 生成的子组件必须是允许包含在LazyForEach父容器组件中的子组件。
  • 允许LazyForEach包含在if/else条件渲染语句中,也允许LazyForEach中出现if/else条件渲染语句。
  • 键值生成器必须针对每个数据生成唯一的值,如果键值相同,将导致键值相同的UI组件渲染出现问题。
  • LazyForEach必须使用DataChangeListener对象进行更新,对第一个参数dataSource重新赋值会异常;dataSource使用状态变量时,状态变量改变不会触发LazyForEach的UI刷新。
  • 为了高性能渲染,通过DataChangeListener对象的onDataChange方法来更新UI时,需要生成不同于原来的键值来触发组件刷新。
  • LazyForEach必须和@Reusable装饰器一起使用才能触发节点复用。使用方法:将@Reusable装饰在LazyForEach列表的组件上,见使用规则

四,LazyForEach 如何实现更新

  • 数据源 绑定LazyForEach 需要与实现了 IDataSource 接口的数据源(如 LazyDataSource)绑定。当数据源发生变化(增、删、改)时,框架会自动触发更新。
  • 观察者模式 :数据源通过 DataChangeListener 通知 LazyForEach 数据变更。只有实际变化的项会触发局部更新,而非重新渲染整个列表。

五,lazyforeach中key重复

lazyforeach可能是一种具有延迟执行特性的遍历方式,它可能会根据具体的实现情况,在真正需要访问元素的时候才执行遍历操作。

key重复出现的问题

  • 渲染效率问题:如果key重复,渲染效率会受到影响。框架无法准确地根据key来判断元素的变更情况。例如,在更新列表时,框架可能会错误地复用元素或者重新渲染不必要的元素,导致性能下降。
  • 状态关联问题:如果列表元素有与之关联的内部状态(如在组件中通过@State等方式设置的状态),key重复可能会导致状态关联混乱。当更新列表时,因为key不能唯一标识元素,可能会出现状态被错误地应用到其他元素上的情况。

解决办法:

确保key的唯一性:

  • 最直接的方法是为每个元素生成一个唯一的key。如果列表中的元素有一个自然的唯一标识符(如数据库中的主键、对象的唯一 ID 等),可以使用这个标识符作为key
  • 如果没有自然的唯一标识符,可以考虑使用一些辅助函数来生成唯一的key。例如,在 JavaScript 中,可以使用uuid库来生成通用唯一识别码(UUID)作为key

六,使用步骤

  • 3 工作中 直接按需修改!!!
ts 复制代码
// 接口的实现 implements

//  接口中定义了一些属性和方法
//  用类 来继承接口的时候  类 必须 写接口中定义过的属性和方法  ->  用来类实现接口

// interface ABC {
//   a: number
//   b: number
//   c: (s: string) => number
// }
//
// class DDDD implements ABC {
//   a: number = 10
//   b: number = 20
//
//   c(s: string) {
//     return 100
//
//   }
// }


// class RRRRR implements IDataSource {
//   totalCount(): number {
//     throw new Error('Method not implemented.')
//   }
//
//   getData(index: number): any {
//     throw new Error('Method not implemented.')
//   }
//
//   registerDataChangeListener(listener: DataChangeListener): void {
//     throw new Error('Method not implemented.')
//   }
//
//   unregisterDataChangeListener(listener: DataChangeListener): void {
//     throw new Error('Method not implemented.')
//   }
// }

// 哪一个才是真正决定页面渲染用到的数组
// 1 listeners
// 2 dataArray 后期要改变的部分 页面渲染的真正的数据!!


class BasicDataSource implements IDataSource {
  // listeners 真正负责监听你的渲染的组件是否在 屏幕之外的
  // 组件 销毁! 创建
  private listeners: DataChangeListener[] = [];
  // 后期要改变的部分 页面渲染的真正的数据!!
  private dataArray: number[] = [];

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

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

  // 该方法为框架侧(LazyForEach 组件内部自己 不是开发者自己)调用,为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);
    })
  }

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

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


@Entry
@Component
struct MyComponent {
  //  LazyForEach 数据源
  private data: BasicDataSource = new BasicDataSource();

  aboutToAppear() {
    for (let i = 0; i <= 20; i++) {
    this . data . pushData (i) 
    }

  }

  build() {
    List({ space: 3 }) {
      LazyForEach(this.data, (item: string) => {
        ListItem() {
          Row() {
            Text(item).fontSize(50)
              .onAppear(() => {
                console.info("appear:" + item)
              })
          }.margin({ left: 10, right: 10 })
        }
      }, (item: string) => item)
    }.cachedCount(5)
  }
}

七,LazyForEach 数据源 修改

八,相关问题

Lazyforeach失效原因

根据可视区域按需渲染数据,容器定高(height:100%)导致可视区域就是所有数据的渲染结果,解决方法就是去掉你的定高,让盒子内容自行展示渲染

牵扯双层滚动(scroller里面嵌套waterflow)

kotlin 复制代码
.height("100%")
.onReachEnd(this.onReachEnd)
.nestedScroll({
  // 滚动组件往末尾端滚动时的嵌套滚动选项 分页滚动方向
  // 页面 往上面移动
  // 父亲先滚
  scrollForward: NestedScrollMode.PARENT_FIRST,
  // 滚动组件往起始端滚动时的嵌套滚动选项
  // 儿子先滚
  scrollBackward: NestedScrollMode.SELF_FIRST
})

在List内使用屏幕闪烁

在List的onScrollIndex方法中调用onDataReloaded有产生屏幕闪烁的风险。 解决办法:用onDatasetChange代替onDataReloaded,不仅可以修复闪屏的问题,还能提升加载性能。

js 复制代码
1.  class MyDataSource extends BasicDataSource {
1.  private dataArray: string[] = [];
1.
1.  public totalCount(): number {
1.  return this.dataArray.length;
1.  }
1.
1.  public getData(index: number): string {
1.  return this.dataArray[index];
1.  }
1.
1.  public pushData(data: string): void {
1.  this.dataArray.push(data);
1.  this.notifyDataAdd(this.dataArray.length - 1);
1.  }
1.
1.  operateData():void {
1.  const totalCount = this.dataArray.length;
1.  const batch=5;
1.  for (let i = totalCount; i < totalCount + batch; i++) {
1.  this.dataArray.push(`Hello ${i}`)
1.  }
1.  // 替换 notifyDataReload
1.  this.notifyDatasetChange([{type:DataOperationType.ADD, index: totalCount-1, count:batch}])
1.  }
1.  }
1.
1.  @Entry
1.  @Component
1.  struct MyComponent {
1.  private moved: number[] = [];
1.  private data: MyDataSource = new MyDataSource();
1.
1.  aboutToAppear() {
1.  for (let i = 0; i <= 10; i++) {
1.  this.data.pushData(`Hello ${i}`)
1.  }
1.  }
1.
1.  build() {
1.  List({ space: 3 }) {
1.  LazyForEach(this.data, (item: string, index: number) => {
1.  ListItem() {
1.  Row() {
1.  Text(item)
1.  .width('100%')
1.  .height(80)
1.  .backgroundColor(Color.Gray)
1.  .onAppear(() => {
1.  console.info("appear:" + item)
1.  })
1.  }.margin({ left: 10, right: 10 })
1.  }
1.  }, (item: string) => item)
1.  }.cachedCount(10)
1.  .onScrollIndex((start, end, center) => {
1.  if (end === this.data.totalCount() - 1) {
1.  console.log('scroll to end')
1.  this.data.operateData();
1.  }
1.  })
1.  }
1.  }
相关推荐
写雨.015 分钟前
鸿蒙定位开发服务
华为·harmonyos·鸿蒙
goto_w5 小时前
uniapp上使用webview与浏览器交互,支持三端(android、iOS、harmonyos next)
android·vue.js·ios·uni-app·harmonyos
别说我什么都不会20 小时前
ohos.net.http请求HttpResponse header中set-ccokie值被转成array类型
网络协议·harmonyos
码是生活20 小时前
鸿蒙开发排坑:解决 resourceManager.getRawFileContent() 获取文件内容为空问题
前端·harmonyos
鸿蒙场景化示例代码技术工程师21 小时前
基于Canvas实现选座功能鸿蒙示例代码
华为·harmonyos
小脑斧爱吃鱼鱼1 天前
鸿蒙项目笔记(1)
笔记·学习·harmonyos
鸿蒙布道师1 天前
鸿蒙NEXT开发对象工具类(TS)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
zhang1062091 天前
HarmonyOS 基础组件和基础布局的介绍
harmonyos·基础组件·基础布局
马剑威(威哥爱编程)1 天前
在HarmonyOS NEXT 开发中,如何指定一个号码,拉起系统拨号页面
华为·harmonyos·arkts
GeniuswongAir1 天前
Flutter极速接入IM聊天功能并支持鸿蒙
flutter·华为·harmonyos