LazyForEach从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。当在滚动容器中使用了LazyForEach,框架会根据滚动容器可视区域按需创建组件,当组件滑出可视区域外时,框架会进行组件销毁回收以降低内存占用。
使用限制
- LazyForEach必须在容器组件内使用,仅有[List]、[Grid]、[Swiper]以及[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
循环渲染过程中,系统会为每个item生成一个唯一且持久的键值,用于标识对应的组件。当这个键值变化时,ArkUI框架将视为该数组元素已被替换或修改,并会基于新的键值创建一个新的组件。
LazyForEach
提供了一个名为keyGenerator
的参数,这是一个函数,开发者可以通过它自定义键值的生成规则。如果开发者没有定义keyGenerator
函数,则ArkUI框架会使用默认的键值生成函数,即(item: Object, index: number) => { return viewId + '-' + index.toString(); }
, viewId在编译器转换过程中生成,同一个LazyForEach组件内其viewId是一致的。
组件创建规则
在确定键值生成规则后,LazyForEach的第二个参数itemGenerator
函数会根据键值生成规则为数据源的每个数组项创建组件。组件的创建包括两种情况:[LazyForEach首次渲染]和[LazyForEach非首次渲染]。
首次渲染
生成不同键值
在LazyForEach首次渲染时,会根据上述键值生成规则为数据源的每个数组项生成唯一键值,并创建相应的组件。
/** BasicDataSource代码见文档末尾附件: string类型数组的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);
}
}
@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)
}
}
在上述代码中,键值生成规则是keyGenerator
函数的返回值item
。在LazyForEach
循环渲染时,其为数据源数组项依次生成键值Hello 0
、Hello 1
… Hello 20
,并创建对应的ListItem
子组件渲染到界面上。
运行效果如下图所示。
图1 LazyForEach正常首次渲染
键值相同时错误渲染
当不同数据项生成的键值相同时,框架的行为是不可预测的。例如,在以下代码中,LazyForEach
渲染的数据项键值均相同,在滑动过程中,LazyForEach
会对划入划出当前页面的子组件进行预加载,而新建的子组件和销毁的原子组件具有相同的键值,框架可能存在取用缓存错误的情况,导致子组件渲染有问题。
/** BasicDataSource代码见文档末尾附件: string类型数组的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);
}
}
@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) => 'same key')
}.cachedCount(5)
}
}
运行效果如下图所示。
图2 LazyForEach存在相同键值
非首次渲染
当LazyForEach
数据源发生变化,需要再次渲染时,开发者应根据数据源的变化情况调用listener
对应的接口,通知LazyForEach
做相应的更新,各使用场景如下。
添加数据
/** BasicDataSource代码见文档末尾附件: string类型数组的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);
}
}
@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
的子组件时,首先调用数据源data
的pushData
方法,该方法会在数据源末尾添加数据并调用notifyDataAdd
方法。在notifyDataAdd
方法内会又调用listener.onDataAdd
方法,该方法会通知LazyForEach
在该处有数据添加,LazyForEach
便会在该索引处新建子组件。
运行效果如下图所示。
图3 LazyForEach添加数据
删除数据
/** BasicDataSource代码见文档末尾附件: string类型数组的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);
}
public deleteData(index: number): void {
this.dataArray.splice(index, 1);
this.notifyDataDelete(index);
}
}
@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, index: number) => {
ListItem() {
Row() {
Text(item).fontSize(50)
.onAppear(() => {
console.info("appear:" + item)
})
}.margin({ left: 10, right: 10 })
}
.onClick(() => {
// 点击删除子组件
this.data.deleteData(this.data.dataArray.indexOf(item));
})
}, (item: string) => item)
}.cachedCount(5)
}
}
当我们点击LazyForEach
的子组件时,首先调用数据源data
的deleteData
方法,该方法会删除数据源对应索引处的数据并调用notifyDataDelete
方法。在notifyDataDelete
方法内会又调用listener.onDataDelete
方法,该方法会通知LazyForEach
在该处有数据删除,LazyForEach
便会在该索引处删除对应子组件。
运行效果如下图所示。
图4 LazyForEach删除数据
交换数据
/** BasicDataSource代码见文档末尾附件: string类型数组的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);
}
public deleteData(index: number): void {
this.dataArray.splice(index, 1);
this.notifyDataDelete(index);
}
public moveData(from: number, to: number): void {
let temp: string = this.dataArray[from];
this.dataArray[from] = this.dataArray[to];
this.dataArray[to] = temp;
this.notifyDataMove(from, to);
}
}
@Entry
@Component
struct MyComponent {
private moved: number[] = [];
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, index: number) => {
ListItem() {
Row() {
Text(item).fontSize(50)
.onAppear(() => {
console.info("appear:" + item)
})
}.margin({ left: 10, right: 10 })
}
.onClick(() => {
this.moved.push(this.data.dataArray.indexOf(item));
if (this.moved.length === 2) {
// 点击交换子组件
this.data.moveData(this.moved[0], this.moved[1]);
this.moved = [];
}
})
}, (item: string) => item)
}.cachedCount(5)
}
}
当我们首次点击LazyForEach
的子组件时,在moved成员变量内存入要移动的数据索引,再次点击LazyForEach
另一个子组件时,我们将首次点击的子组件移到此处。调用数据源data
的moveData
方法,该方法会将数据源对应数据移动到预期的位置并调用notifyDataMove
方法。在notifyDataMove
方法内会又调用listener.onDataMove
方法,该方法通知LazyForEach
在该处有数据需要移动,LazyForEach
便会将from
和to
索引处的子组件进行位置调换。
运行效果如下图所示。
图5 LazyF