在 ArkTS 的开发中,如果你要渲染一个很长的列表,比如商品列表、评论列表或者朋友圈动态,用传统的循环结构(比如 ForEach
)很容易导致性能问题,尤其是加载慢、卡顿甚至内存暴涨。
这时候就要用到 懒加载渲染组件——LazyForEach
。
LazyForEach
是 ArkTS 提供的一种 延迟渲染列表项的方式。只有当列表项真正要被显示在屏幕上时,相关组件才会被创建和渲染,从而节省内存和提升性能。
可以把它理解成 ArkTS 中的“虚拟滚动列表”。
LazyForEach的使用限制
- LazyForEach必须在容器组件内使用,仅有List、Grid、Swiper以及WaterFlow组件支持数据懒加载(可配置cachedCount属性,即只加载可视部分以及其前后少量数据用于缓冲),其他组件仍然是一次性加载所有的数据。支持数据懒加载的父组件根据自身及子组件的高度或宽度计算可视区域内需布局的子节点数量,高度或宽度的缺失会导致部分场景懒加载失效。
catchdCount
List设置cachedCount后,显示区域外上下各会预加载并布局cachedCount行ListItem。
- 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键值生成规则
LazyForEach提供了参数keyGenerator,开发者可以使用该函数生成自定义键值。如果未定义keyGenerator函数,ArkUI框架将使用默认的键值生成函数:(item: Object, index: number) => { return viewId + ‘-’ + index.toString(); }。viewId在编译器转换过程中生成,同一个LazyForEach组件内的viewId一致。
基本语法
LazyForEach(
dataSource: IDataSource, // 需要进行数据迭代的数据源
itemGenerator: (item: any, index?: number) => void, // 子组件生成函数
keyGenerator?: (item: any, index?: number) => string // 键值生成函数
): void
dataSource:IDataSource
LazyForEach数据源,需要开发者实现相关接口。
itemGenerator:(item: any, index: number) => void
子组件生成函数,为数组中的每一个数据项创建一个子组件。
keyGenerator: (item: any, index: number) => string
键值生成函数,用于给数据源中的每一个数据项生成唯一且固定的键值。修改数据源中的一个数据项若不影响其生成的键值,则对应组件不会被更新,否则此处组件就会被重建更新。keyGenerator参数是可选的,但是,为了使开发框架能够更好地识别数组更改并正确更新组件,建议提供。
实现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; // 改变数据完成后调用 }
创建LazyForEach
首次渲染
在LazyForEach首次渲染时,会根据上述键值生成规则为数据源的每个数组项生成唯一键值并创建相应的组件。
局部代码如下:
class MyDataSource extends BasicDataSource {
private dataArray: string[] = [];
public totalCount(): number {
return this.dataArray.length;
}
public getData(index: number): string {
return this.dataArray[index];
}
public pushData(data: string): void {
this.dataArray.push(data);
this.notifyDataAdd(this.dataArray.length - 1);
}
}
@Entry
@Component
struct MyComponent {
private data: MyDataSource = new MyDataSource();
aboutToAppear() {
for (let i = 0; i <= 20; i++) {
this.data.pushData(`Hello ${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数据源发生变化,需要再次渲染时,开发者应根据数据源的变化情况调用listener对应的接口,通知LazyForEach做相应的更新。
局部代码如下:
class MyDataSource extends BasicDataSource {
private dataArray: string[] = [];
public totalCount(): number {
return this.dataArray.length;
}
public getData(index: number): string {
return this.dataArray[index];
}
public pushData(data: string): void {
this.dataArray.push(data);
this.notifyDataAdd(this.dataArray.length - 1);
}
}
@Entry
@Component
struct MyComponent {
private data: MyDataSource = new MyDataSource();
aboutToAppear() {
for (let i = 0; i <= 20; i++) {
this.data.pushData(`Hello ${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 })
}
.onClick(() => {
// 点击追加子组件
this.data.pushData(`Hello ${this.data.totalCount()}`);
})
}, (item: string) => item)
}.cachedCount(5)
}
}
渲染结果如图:
LazyForEach之前:先实现IDataSource接口
// 实现IDataSource接口,用于管理listener监听,以及通知LazyForEach数据更新
class BasicDataSource implements IDataSource {
private listeners: DataChangeListener[] = [];
private originDataArray: string[] = [];
public totalCount(): number {
return this.originDataArray.length;
}
public getData(index: number): string {
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);
// 写法2:listener.onDatasetChange([{type: DataOperationType.ADD, index: index}]);
});
}
// 通知LazyForEach组件在index对应索引处数据有变化,需要重建该子组件
notifyDataChange(index: number): void {
this.listeners.forEach(listener => {
listener.onDataChange(index);
// 写法2:listener.onDatasetChange([{type: DataOperationType.CHANGE, index: index}]);
});
}
// 通知LazyForEach组件需要在index对应索引处删除该子组件
notifyDataDelete(index: number): void {
this.listeners.forEach(listener => {
listener.onDataDelete(index);
// 写法2:listener.onDatasetChange([{type: DataOperationType.DELETE, index: index}]);
});
}
// 通知LazyForEach组件将from索引和to索引处的子组件进行交换
notifyDataMove(from: number, to: number): void {
this.listeners.forEach(listener => {
listener.onDataMove(from, to);
// 写法2:listener.onDatasetChange(
// [{type: DataOperationType.EXCHANGE, index: {start: from, end: to}}]);
});
}
notifyDatasetChange(operations: DataOperation[]): void {
this.listeners.forEach(listener => {
listener.onDatasetChange(operations);
});
}
}