一 概述
LazyForEach,框架会根据滚动容器可视区域按需创建组件,当组件滑出可视区域外时,框架会进行组件销毁回收以降低内存占用。
,懒加载LazyForEach是一种延迟加载的技术,它是在需要的时候才加载数据或资源,并在每次迭代过程中创建相应的组件,而不是一次性将所有内容都加载出来。懒加载通常应用于长列表、网格、瀑布流等数据量较大、子组件可重复使用的场景,当用户滚动页面到相应位置时,才会触发资源的加载,以减少组件的加载时间,提高应用性能,提升用户体验。
二 LazyForEach和ForEach对比
2.1 ForEach循环渲染的过程如下:
- 从列表数据源一次性加载全量数据。
- 为列表数据的每一个元素都创建对应的组件,并全部挂载在组件树上。即,ForEach遍历多少个列表元素,就创建多少个ListItem组件节点并依次挂载在List组件树根节点上。
- 列表内容显示时,只渲染屏幕可视区内的ListItem组件,可视区外的ListItem组件滑动进入屏幕内时,因为已经完成了数据加载和组件创建挂载,直接渲染即可。
2.2 LazyForEach
实现了按需加载,针对列表数据量大、列表组件复杂的场景,减少了页面首次启动时一次性加载数据的时间消耗,减少了内存峰值。不过在长列表滑动的过程中,因为需要根据用户的滑动行为不断地加载新的内容,这需要进行额外的数据请求和处理,会增加滑动时的计算量,从而对性能产生一定的影响。然而,合理使用LazyForEach的按需加载能力,通过在滑动停止或达到某个阈值时才进行加载,可以减少不必要的计算和请求,从而提高性能,给用户带来更好的体验。总之,在实现按需加载的场景中,需要综合考虑性能和用户体验的平衡,合理地优化加载逻辑和渲染方式,以提升整体的性能表现。
LazyForEach使用
- 仅有List、Grid、Swiper以及WaterFlow组件支持数据懒加载(可配置
cachedCount属性(只有在LazyForEach有效
),即只加载可视部分以及其前后少量数据用于缓冲) - LazyForEach在每次迭代中,必须创建且只允许创建一个子组件。
- 生成的子组件必须是允许包含在LazyForEach父容器组件中的子组件。
- 允许LazyForEach包含在if/else条件渲染语句中,也允许LazyForEach中出现if/else条件渲染语句。
- 键值生成器必须针对每个数据生成唯一的值,如果键值相同,将导致键值相同的UI组件渲染出现问题。
- LazyForEach必须使用DataChangeListener对象来进行更新,第一个参数dataSource使用状态变量时,状态变量改变不会触发LazyForEach的UI刷新。
- 为了高性能渲染,通过DataChangeListener对象的onDataChange方法来更新UI时,需要生成不同于原来的键值来触发组件刷新。
- LazyForEach必须和@Reusable装饰器一起使用才能触发节点复用。使用方法:将@Reusable装饰在LazyForEach列表的组件上,见使用规则。
键值生成函数keyGenerator中不推荐使用stringify。在复杂的业务场景中,使用stringify会对item对象进行序列化,最终把item转换成字符串,这过程需要消耗大量的时间和计算资源,从而导致页面性能降低
三 LazyForEach 使用方法
必须配合 IDataSource 来使用 和 cachedCount(建议使用) 需要定义两个 BasicDataSource(来存原始数据)和 一个 继承 BasicDataSource 咱们自己的DataSource,
3.0 键值生成规则
如果开发者没有定义 keyGenerato r函数,则ArkUI框架会使用默认的键值生成函数,即(item: Object, index: number) => { return viewId + '-' + index.toString(); }, viewId在编译器转换过程中生成,同一个LazyForEach组件内其viewId是一致的。相当于就是跟index有关
,所以我们必须定义 keyGenerato r函数,并且避免,JSON.stringify(item)
,原因是当数据过大的时候,影响性能
3.1 IDataSource 方法
下面的方法一般是固定写法
- totalCount:获得数据总数。
- getData:获取索引值index对应的数据。
- registerDataChangeListener:注册数据改变的监听器。该方法为框架侧调用,为LazyForEach组件向其数据源处添加listener监听
- unregisterDataChangeListener:注销数据改变的监听器。该方法为框架侧调用,为LazyForEach组件向其数据源处移除listener监听
export 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) {
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);
}
}
}
3.2 DataChangeListener
因为 onDataReloaded 、onDataAdd、onDataMove、onDataDelete、onDataChange 和 listener.onDatasetChange() 这两个不能同时使用,否则会报错 Error message:onDatasetChange cannot be used with other interface
- onDataReloaded 建议使用
listener.onDatasetChange([{type: DataOperationType.RELOAD}]
通知组件重新加载所有数据。键值没有变化的数据项会使用原先的子组件,键值发生变化的会重建子组件。重新加载数据完成后调用。 - onDataAdd 建议使用
listener.onDatasetChange([{type:DataOperationType.ADD, index: index}]);
通知组件index的位置有数据添加。添加数据完成后调用 - onDataMove(from: number, to: number):建议使用
listener.onDatasetChange([{ type: DataOperationType.MOVE, index: { from: from, to: to } }]
; )通知组件数据有移动。将from和to位置的数据进行交换。数据移动起始位置与数据移动目标位置交换完成后调用。 - onDataDelete:建议使用
listener.onDatasetChange([{type: DataOperationType.DELETE, index: index}]);
通知组件删除index位置的数据并刷新LazyForEach的展示内容。删除数据完成后调用 - onDataChange(index: number): void 建议使用
listener.onDatasetChange([{type: DataOperationType.CHANGE, index: index}]);
通知组件index的位置有数据有变化。改变数据完成后调用。 - onDatasetChange 方法 这个比较多
3.2.1 onDatasetChange
onDatasetChange(dataOperations: DataOperation[])
3.2.1.1 DataOperation
-
DataAddOperation:添加数据操作。即 {type: DataOperationType.ADD, index: index}
-
DataDeleteOperation:删除数据操作。即 {type: DataOperationType.DELETE, index: index}
-
DataChangeOperation:改变数据操作。即 {type: DataOperationType.CHANGE, index: index}
-
DataMoveOperation:移动数据操作 即 { type: DataOperationType.MOVE, index: { from: from, to: to } }
-
DataExchangeOperation:交换数据操作。即 {type: DataOperationType.CHANGE, index: index}
-
DataReloadOperation:重载所有数据操作。当onDatasetChange含有DataOperationType.RELOAD操作时,其余操作全部失效,框架会自己调用keygenerator进行键值比对 即 {type: DataOperationType.RELOAD}
封装一个BaseDataSource
onDataReloaded 、onDataAdd、onDataMove、onDataDelete、onDataChange 和 listener.onDatasetChange() 所以,都是用 onDatasetChange 方式 主要方法有
- addData(data: T, index?: number),向数组末尾添加一个元素或者是指定位置添加一个元素
- addAllData(data:
Array<T>
) 在末尾添加一个数组 - setNewData(data:
Array<T>
) 添加全新的数据,也就是清空原有的数据,添加新的数组 - deleteData(index: number) 删除 index 下的数组,如果 index 大于或者等于 ,则不删除,也不添加
- moveData(from: number, to: number) 移动数据,如果 from to 大于或者等于 则不移动
- notifyItemChange(index: number, data: T) 通知单个item更改,如果 大于或者等于 数组的length 就不操作
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];
}
// 系统调用,开发不用关心
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
this.listeners.push(listener);
}
}
// 系统调用,开发不用关心
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
this.listeners.splice(pos, 1);
}
}
// 通知LazyForEach组件需要重载所有子组件,这个有可能闪烁
// 用 onDatasetChange 代替onDataReloaded,不仅可以修复闪屏的问题,还能提升加载性能
// https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-rendering-control-lazyforeach-V5
// 通知组件重新加载所有数据。键值没有变化的数据项会使用原先的子组件,键值发生变化的会重建子组件。重新加载数据完成后调用。
notifyDataReload(): void {
this.listeners.forEach(listener => {
listener.onDatasetChange([{ type: DataOperationType.RELOAD }]);
})
}
notifyDatasetChange(operations: DataOperation[]): void {
this.listeners.forEach(listener => {
listener.onDatasetChange(operations);
})
}
}
export class BaseDataSource<T> extends BasicDataSource<T> {
private dataArray: T[] = [];
public totalCount(): number {
return this.dataArray.length;
}
/**
* 获取数据
* @param index
* @returns
*/
public getData(index: number): T {
return this.dataArray[index];
}
private addIndexData(index: number, data: T): void {
if (this.dataArray.length >= index) {
this.dataArray.splice(index, 0, data);
this.notifyDatasetChange([
{ type: DataOperationType.ADD, index: index }])
} else {
console.warn(`addData 当前的index=${index} 已经大于 数组的length=${this.dataArray.length}`)
}
}
/**
* 删除 index 下的数组,如果 index 大于或者等于 ,则不删除,也不添加
* @param index
* @returns 是否成功
*/
public deleteData(index: number): boolean {
if (this.dataArray.length > index) {
this.dataArray.splice(index, 1);
this.notifyDatasetChange([
{ type: DataOperationType.DELETE, index: index }
])
return true;
}
console.warn(`deleteData 当前的index=${index} 已经大于或者等于 数组的length=${this.dataArray.length}`)
return false;
}
/**
* 移动数据,如果 from to 大于或者等于 则不移动
* @param from 要移动的index
* @param to 要移动到的目标index
* @returns 是否成功
*/
public moveData(from: number, to: number): boolean {
if (this.dataArray.length > from && this.dataArray.length > to) {
this.notifyDatasetChange([
{ type: DataOperationType.MOVE, index: { from: from, to: to } }
])
return true
}
console.warn(`moveData 当前的from=${from} to=${to} 已经大于或者等于 数组的length=${this.dataArray.length}`)
return false
}
/**
* 向数组末尾添加一个元素
* @param arr 要添加元素的数组
* @param element 要添加的元素
* @returns 返回添加元素后的数组
*/
public addData(data: T, index?: number): void {
if (index) {
this.addIndexData(index, data);
} else {
this.dataArray.push(data);
this.notifyDatasetChange([
{ type: DataOperationType.ADD, index: this.dataArray.length - 1 }])
}
}
/**
* 通知单个item更改
* 如果是新的数据,那么相当于重新设置了数据,如果里面有图片 ,图片会闪一下
* 可以使用 @Observed 和 @ObjectLink 配合使用去更新数据,避免闪烁
* @param index
* @param data
* @returns 是否成功
*/
public notifyItemChange(index: number, data: T): boolean {
// 是否需要这样,这个的本意就是通知当前item改变了
if (this.dataArray.length > index) {
// 但是如果没有这个index ,这里会增加
this.dataArray.splice(index, 1, data);
this.notifyDatasetChange([
{ type: DataOperationType.CHANGE, index: index }])
return true;
}
console.warn(`notifyItemChange 当前的index=${index} 已经大于或者等于 数组的length=${this.dataArray.length}`)
return false;
}
/**
* 在末尾添加一个数组
* @param data
*/
public addAllData(data: Array<T>): void {
const totalCount = this.dataArray.length;
this.dataArray.push(...data);
this.notifyDatasetChange([
{ type: DataOperationType.ADD, index: totalCount, count: data.length }])
}
/**
* 添加全新的数据,也就是清空原有的数据,添加新的数组
* @param data
*/
public setNewData(data: Array<T>) {
let oldLength = this.dataArray.length
this.dataArray.splice(0, this.dataArray.length);
this.dataArray.push(...data)
this.notifyDatasetChange([
{ type: DataOperationType.DELETE, index: 0, count: oldLength },
{ type: DataOperationType.ADD, index: 0, count: data.length },
// 这样可以服用以前的数据,比如 setNewData 两次同样的数据,如果不加这个会闪烁
{ type: DataOperationType.RELOAD },
])
}
}