一 介绍
LazyForEach从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。当在滚动容器中使用了LazyForEach,框架会根据滚动容器可视区域按需创建组件,当组件滑出可视区域外时,框架会进行组件销毁回收以降低内存占用。
简而言之,其实它和前端的虚拟列表基本一样,只渲染可见区域的数据,来提升页面的响应速度,减少卡顿。
二 使用介绍
接口描述
LazyForEach(
dataSource: IDataSource, // 需要进行数据迭代的数据源
itemGenerator: (item: any, index: number) => void, // 子组件生成函数
keyGenerator?: (item: any, index: number) => string // 键值生成函数
): void
LazyForEach是一个函数接口,该函数接收三个参数:dataSource(必填),itemGenerator(必填),keyGenerator(可选)。
以下我也将简单介绍这三个参数的使用。
dataSource
类型为 IDataSource 的数据源,需要开发者实现相关接口。该参数提供需要进行遍历和处理的数据集合。而我们要了解数据源如何定义,就不得不提及类型IDataSource和数据变化监听器DataChangeListener。
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; // 改变数据完成后调用
}
itemGenerator
子组件生成函数,为数组中的每一个数据项创建一个子组件。其参数为当前数据项 item(类型为 any)和该数据项的索引值 index(类型为 number)。
此函数的目的是为每个数据项生成一个子组件。函数体必须使用大括号 {...} 包裹,且每次迭代必须生成一个子组件。在 itemGenerator 中,可以使用条件语句(如 if),但必须确保每个条件分支都会创建。
keyGenerator
键值生成函数,用于为数据源中的每个数据项生成唯一且固定的键值。其参数同样为当前数据项 item 和索引值 index。数据源中的每一个数据项生成的键值不能重复。
当数据项在数组中的位置更改时,其键值不得更改,当数组中的数据项被新项替换时,被替换项的键值和新项的键值必须不同。键值生成器的功能是可选的,但是,为了使开发框架能够更好地识别数组更改,提高性能,建议提供。如将数组反向时,如果没有提供键值生成器,则LazyForEach中的所有节点都将重建。
注:本参数虽然为可选参数,但如果你不做设置的话,当LazyForEach数据源发生变化,需要再次渲染时,LazyForEach渲染的数据项键值会均相同。在滑动过程中,LazyForEach会对划入划出当前页面的子组件进行预加载,而新建的子组件和销毁的原子组件具有相同的键值,框架可能存在取用缓存错误的情况,导致子组件渲染有问题。
因为为了高性能渲染,通过DataChangeListener对象的onDataChange方法来更新UI时,需要生成不同于原来的键值才触发组件刷新。
键值生成规则
在LazyForEach循环渲染过程中,系统会为每个item生成一个唯一且持久的键值,用于标识对应的组件。当这个键值变化时,ArkUI框架将视为该数组元素已被替换或修改,并会基于新的键值创建一个新的组件。
LazyForEach提供了一个名为keyGenerator的参数,这是一个函数,开发者可以通过它自定义键值的生成规则。如果开发者没有定义keyGenerator函数,则ArkUI框架会使用默认的键值生成函数,即
(item: any, index: number) => {
return viewId + '-' + index.toString();
},
viewId在编译器转换过程中生成,同一个LazyForEach组件内其viewId是一致的。
使用限制
LazyForEach必须在容器组件内使用,仅有List、Grids、Swiper以及WaterFlow组件支持数据懒加载(可配置cachedCount属性,即只加载可视部分以及其前后少量数据用于缓冲),其他组件仍然是一次性加载所有的数据。
LazyForEach在每次迭代中,必须创建且只允许创建一个子组件。
生成的子组件必须是允许包含在LazyForEach父容器组件中的子组件。
允许LazyForEach包含在if/else条件渲染语句中,也允许LazyForEach中出现if/else条件渲染语句。
键值生成器必须针对每个数据生成唯一的值,如果键值相同,将导致键值相同的UI组件渲染出现问题。
LazyForEach必须使用DataChangeListener对象来进行更新,第一个参数dataSource使用状态变量时,状态变量改变不会触发LazyForEach的UI刷新。
三 方法实现
封装泛型类
在BasicDataSource类的实现中,泛型T被用作originDataArray的类型参数,确保存储的数据元素与期望的类型一致,同时getData方法也返回类型T的元素,保证了类型的一致性。
// Basic implementation of IDataSource to handle data listener
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) {
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);
})
}
}
ListDataSource是继承自BasicDataSource的泛型类,用于管理特定类型的数据列表。它具有totalCount()获取数据总数,getData()按索引获取数据,addData()在指定位置插入数据并通知监听器,以及pushData()在列表末尾添加数据并通知的功能。
// 基础类
export class ListDataSource<T> extends BasicDataSource<T> {
private dataArray: T[] = [];public totalCount(): number {
return this.dataArray.length;
}public getData(index: number): T {
return this.dataArray[index];
}public addData(index: number, data: T): void {
this.dataArray.splice(index, 0, data);
this.notifyDataAdd(index);
}public pushData(data: T): void {
this.dataArray.push(data);
this.notifyDataAdd(this.dataArray.length - 1);
}
}
以及为了满足下拉刷新的业务需求,自定义了一个reloadData方法,用于重新加载数据。此方法会将数组dataArray重置为空数组,再调用notifyDataReload函数,通知所有注册的数据监听器进行数据更新操作。便于之后LazyForEach重新自动渲染数据。
public reloadData () {
this.dataArray = [] // 不会引起状态变化
// 必须通过DataChangeListener来更新
this.notifyDataReload() // 重新加载数据
}
注:当这些基本方法不能满足我们的需求时,可以自定义方法来实现,记得调用notifyDataReload() 重新加载数据。
数据渲染
这是基于官方案例的简单展现,代码如下。
import { ListDataSource } from '../models';
class sun {
name: string = 'hello'
sum: number = 0constructor(name: string, sum: number) {
this.name = name;
this.sum = sum;
}
}
@Entry
@Component
struct MyComponent {
@State data: ListDataSource<sun> = new ListDataSource();aboutToAppear() {
for (let i = 0; i <= 100; i++) {
this.data.pushData(new sun('hello',i))
}
}build() {
List({ space: 3 }) {
LazyForEach(this.data, (item: sun) => {
ListItem() {
Row({space:10}) {
Text(item.name ).fontSize(50)
Text(item.sum.toString()).fontSize(50)
}.margin({ left: 10, right: 10 })
}
}, (item: sun) => item.sum.toString())
}.cachedCount(5)
}
}
这是真实场景中的部分代码。
import { getArticleList } from '../../api'
import { ArticleItem, ListDataSource } from '../../models'
import { promptAction } from '@kit.ArkUI'@Component
struct Home {
@State
refresh: boolean = false
@State
list: ListDataSource<ArticleItem> = new ListDataSource<ArticleItem>()
@State
loading: boolean = false
@State
pre_timestamp: number = 0
@State
refreshStatus: RefreshStatus = RefreshStatus.Inactive
@State
finished: boolean = false // 是否已经加载完成list1: ListDataSource<ArticleItem> = new ListDataSource<ArticleItem>()
async getArticleList() {
const result = await getArticleList({ channel_id: 0, timestamp: this.pre_timestamp || Date.now() })
this.pre_timestamp = result.pre_timestamp
result.results.forEach(item => {
this.list.pushData(item)
})
this.finished = !this.pre_timestamp
}async reload() {
this.finished = false
const result = await getArticleList({ channel_id: 0, timestamp: Date.now() })
this.pre_timestamp = result.pre_timestamp
this.list.reloadData() // 清空数据
result.results.forEach(item => {
this.list.pushData(item)
})
}
@Builder
getArticleItem(item: ArticleItem) {
Column({ space: 10 }) {
if (item.cover.type === 1) {
Row() {
Text(item.title)
.fontSize(16)
Image(item.cover.images[0])
.width(100)
.height(60)
.borderRadius(4)
}
.justifyContent(FlexAlign.SpaceBetween)
.width("100%")
.alignItems(VerticalAlign.Top)
} else if (item.cover.type === 3) {
Column({ space: 10 }) {
Text(item.title)
.fontSize(16)
Row({ space: 10 }) {
ForEach(item.cover.images, (url: string, index: number) => {
Image(url)
.layoutWeight(1)
.height(60)
.borderRadius(4)
})
}
.width("100%")
}
.width("100%")
.alignItems(HorizontalAlign.Start)
} else {
Text(item.title)
.fontSize(16)
.width("100%")
}
Row({ space: 10 }) {
Text(item.aut_name)
.fontColor(Color.Gray)
.fontSize(12)
Text(`${item.comm_count}评论`)
.fontColor(Color.Gray)
.fontSize(12)
Text(item.pub_date)
.fontColor(Color.Gray)
.fontSize(12)
}
.width("100%")
}
.width("100%")
.padding(20)
.border({
color: Color.Gray,
width: {
bottom: 0.5
}
})
}getTextByStatus() {
switch (this.refreshStatus) {
case RefreshStatus.Drag:
return "继续下拉"
case RefreshStatus.OverDrag:
return "松手加载"
case RefreshStatus.Refresh:
return "加载中"
}
return ""
}@Builder
getRefreshBuilder() {
Row({ space: 20 }) {
Text(this.getTextByStatus())
.fontSize(12)
.fontColor(Color.Red)
LoadingProgress()
.width(20)
.height(20)
.color(Color.Red)
}
.height(50)
.justifyContent(FlexAlign.Center)
.width("100%")
}build() {
// 下拉刷新 Refresh
// 上拉加载 List
Refresh({ refreshing: $$this.refresh, builder: this.getRefreshBuilder }) {
List() {
LazyForEach(this.list, (item: ArticleItem) => {
ListItem() {
this.getArticleItem(item)
}
}, (item: ArticleItem) => item.art_id)
if (this.finished) {
ListItem() {
Row() {
Text("没啦")
.fontSize(12)
}
.width("100%")
.height(40)
.justifyContent(FlexAlign.Center)
}
} else {
if (this.loading) {
// 正在加载中
ListItem() {
Row({ space: 10 }) {
Text("加载数据中")
.fontSize(12)
LoadingProgress()
.width(20)
.height(20)
}
.width("100%")
.height(40)
.justifyContent(FlexAlign.Center)
}
}
}
}
.onReachEnd(async () => {
// 阀门控制
if (!this.loading && !this.finished) {
this.loading = true
await this.getArticleList()
this.loading = false
}})
}
.onStateChange(async (status) => {
this.refreshStatus = status
if (this.refreshStatus === RefreshStatus.Refresh) {
await this.reload()
this.refresh = false // 手动关闭状态
promptAction.showToast({ message: '刷新成功' })
}
})
// .onRefreshing() // 只会在超过一定区域 松手时触发
}
}export default Home
四 结语
因为我们使用LazyForEach也主要是用于大量列表数据的渲染的场景,是为了解决使用ForEach循环数据列表中长期下拉添加数据导致的页面卡顿,不流畅问题,同时实现项目性能优化,所以本文主要介绍的就是LazyForEach的循环数据源,和自定义清空实现下拉刷新。
所以添加,删除,交换,改变数据等操作的实际使用情景,请参考文档自己实现。(抱歉...)