文章目录
一、简介(LazyForEach)
懒加载LazyForEach是一种延迟加载的技术,它是在需要的时候才加载数据或资源,并在每次迭代过程中创建相应的组件,而不是一次性将所有内容都加载出来。懒加载通常应用于长列表、网格、瀑布流等数据量较大、子组件可重复使用的场景,当用户滚动页面到相应位置时,才会触发资源的加载,以减少组件的加载时间,提高应用性能,提升用户体验。
二、原理介绍
在声明式描述语句中,有两种方式控制列表、网格等容器类组件的渲染,分别为循环渲染(ForEach)和数据懒加载(LazyForEach)。
1、循环渲染(ForEach)
-
从列表数据源一次性加载全量数据。
-
为列表数据的每一个元素都创建对应的组件,并全部挂载在组件树上。即,ForEach遍历多少个列表元素,就创建多少个ListItem组件节点并依次挂载在List组件树根节点上。
-
列表内容显示时,只渲染屏幕可视区内的ListItem组件,可视区外的ListItem组件滑动进入屏幕内时,因为已经完成了数据加载和组件创建挂载,直接渲染即可。
其数据加载、组件树挂载、页面渲染的示意图如下所示:
如果列表数据较少,数据一次性全量加载不是性能瓶颈时,可以直接使用ForEach;但是当数据量大、组件结构复杂的情况下ForEach会出现性能瓶颈。这是因为要一次性加载所有的列表数据,创建所有组件节点并完成组件树的构建,在数据量大时会非常耗时,从而导致页面启动时间过长。另外,屏幕可视区外的组件虽然不会显示在屏幕上,但是仍然会占用内存。在系统处于高负载的情况下,更容易出现性能问题,极限情况下甚至会导致应用异常退出。
2、数据懒加载(LazyForEach)
-
LazyForEach会根据屏幕可视区能够容纳显示的组件数量按需加载数据。
-
根据加载的数据量创建组件,挂载在组件树上,构建出一棵短小的组件树。即,屏幕可以展示多少列表项组件,就按需创建多少个ListItem组件节点挂载在List组件树根节点上。
-
屏幕可视区只展示部分组件。当可视区外的组件需要在屏幕内显示时,需要从头完成数据加载、组件创建、挂载组件树这一过程,直至渲染到屏幕上。
其数据加载、组件树挂载、页面渲染的示意图如下所示:
LazyForEach实现了按需加载,针对列表数据量大、列表组件复杂的场景,减少了页面首次启动时一次性加载数据的时间消耗,减少了内存峰值。不过在长列表滑动的过程中,因为需要根据用户的滑动行为不断地加载新的内容,这需要进行额外的数据请求和处理,会增加滑动时的计算量,从而对性能产生一定的影响。然而,合理使用LazyForEach的按需加载能力,通过在滑动停止或达到某个阈值时才进行加载,可以减少不必要的计算和请求,从而提高性能,给用户带来更好的体验。
三、使用方法(LazyForEach)
1、接口描述
LazyForEach(
dataSource: IDataSource, // 需要进行数据迭代的数据源
itemGenerator: (item: any, index: number) => void, // 子组件生成函数
keyGenerator?: (item: any, index: number) => string // 键值生成函数
): void
2、参数
参数名 | 参数类型 | 必填 | 参数描述 |
---|---|---|---|
dataSource | IDataSource | 是 | LazyForEach数据源,需要开发者实现相关接口。 |
itemGenerator | (item: any, index:number) => void | 是 | 子组件生成函数,为数组中的每一个数据项创建一个子组件。 |
keyGenerator | (item: any, index:number) => string | 否 | 键值生成函数,用于给数据源中的每一个数据项生成唯一且固定的键值。 |
3、IDataSource类型说明
interface IDataSource {
totalCount(): number; // 获得数据总数
getData(index: number): Object; // 获取索引值对应的数据
registerDataChangeListener(listener: DataChangeListener): void; // 注册数据改变的监听器
unregisterDataChangeListener(listener: DataChangeListener): void; // 注销数据改变的监听器
}
4、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必须在容器组件内使用,仅有List、Grid、Swiper以及WaterFlow组件支持数据懒加载(可配置cachedCount属性,即只加载可视部分以及其前后少量数据用于缓冲),其他组件仍然是一次性加载所有的数据。
-
LazyForEach在每次迭代中,必须创建且只允许创建一个子组件。
-
生成的子组件必须是允许包含在LazyForEach父容器组件中的子组件。
-
允许LazyForEach包含在if/else条件渲染语句中,也允许LazyForEach中出现if/else条件渲染语句。
-
键值生成器必须针对每个数据生成唯一的值,如果键值相同,将导致键值相同的UI组件渲染出现问题。
-
LazyForEach必须使用DataChangeListener对象来进行更新,第一个参数dataSource使用状态变量时,状态变量改变不会触发LazyForEach的UI刷新。
-
为了高性能渲染,通过DataChangeListener对象的onDataChange方法来更新UI时,需要生成不同于原来的键值来触发组件刷新。
五、使用场景(LazyForEach)
LazyForEach作为常见的渲染控制的方式之一,常用的使用场景有长列表加载、无限瀑布流等。
1、长列表加载
长列表作为应用开发中最常见的开发场景之一,通常会包含成千上万个列表项,在此场景下,直接使用循环渲染ForEach一次性加载所有的列表项,会导致渲染时间过长,影响用户体验。而使用数据懒加载LazyForEach替换循环渲染ForEach,可以按需加载列表项,从而提升列表性能。
虽然,按需加载列表项可以优化长列表性能,但在快速滑动长列表的场景下,可能会来不及加载需要显示的列表项,导致出现白块的现象,从而影响用户体验。而在ArkUI中,List容器提供了cachedCount属性,LazyForEach可以结合cachedCount属性一起使用,能够避免白块的现象。cachedCount可以设置列表中ListItem/ListItemGroup的预加载数量,并且只在LazyForEach中生效,即cachedCount只能与LazyForEach一起使用。除了List容器,其他容器Grid、Swiper以及WaterFlow也都包含cachedCount属性。cachedCount的使用方法如下所示。
List() {
// ...
}.cachedCount(3)
2、无限瀑布流
瀑布流的内容呈现方式类似瀑布流一样,从上往下依次排列,每一列的高度不一定相同,整体呈现出瀑布流的视觉效果。在瀑布流中,经常使用LazyForEach实现数据按需加载,同时,结合onReachEnd、onApear方法实现无限瀑布流。
虽然在onReachEnd()触发时新增数据可以实现无限加载,但在滑动到底部时,会有明显的停顿加载新数据的过程。
想要流畅的进行无限滑动,还需要调整下增加新数据的时机。比如可以在LazyForEach还剩若干个数据就迭代到结束的情况下提前增加一些新数据。
六、瀑布流组件(WaterFlow)
瀑布流容器,由“行”和“列”分割的单元格所组成,通过容器自身的排列规则,将不同大小的“项目”自上而下,如瀑布般紧密布局。
1、接口
WaterFlow(options?: {
footer?: CustomBuilder, scroller?: Scroller})
2、参数
参数名 | 参数类型 | 必填 | 参数描述 |
---|---|---|---|
footer | CustomBuilder | 否 | 设置WaterFlow尾部组件。 |
scroller | scroller | 否 | 可滚动组件的控制器,与可滚动组件绑定。 |
3、属性
名称 | 参数类型 | 描述 |
---|---|---|
columnsTemplate | string | 设置当前瀑布流组件布局列的数量,不设置时默认1列。 例如, ‘1fr 1fr 2fr’ 是将父组件分3列,将父组件允许的宽分为4等份,第一列占1份,第二列占1份,第三列占2份。并支持auto-fill。 默认值:‘1fr’ |
rowsTemplate | string | 设置当前瀑布流组件布局行的数量,不设置时默认1行。 |
itemConstraintSize | ConstraintSizeOptions | 设置约束尺寸,子组件布局时,进行尺寸范围限制。 |
columnsGap | Length | 设置列与列的间距。 默认值:0 |
rowsGap | Length | 设置行与行的间距。 默认值:0 |
layoutDirection | FlexDirection | 设置布局的主轴方向。 默认值:FlexDirection.Column |
3、事件
名称 | 功能描述 |
---|---|
onReachStart(event: () => void) | 瀑布流组件到达起始位置时触发。 |
onReachEnd(event: () => void) | 瀑布流组件到底末尾位置时触发。 |
七、使用示例(无限瀑布流)
1、实现代码
// WaterFlowDataSource.ets
// 实现IDataSource接口的对象,用于瀑布流组件加载数据
export class WaterFlowDataSource implements IDataSource {
private dataArray: number[] = []
private listeners: DataChangeListener[] = []
// 获取索引对应的数据
public getData(index: number): any {
return this.dataArray[index]
}
// 获取数据总数
public totalCount(): number {
return this.dataArray.length
}
// 注册改变数据的控制器
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)
}
}
// 通知控制器数据增加
notifyDataAdd(index: number): void {
this.listeners.forEach(listener => {
listener.onDataAdd(index)
})
}
//增加数据
public pushData(data: number): void {
this.dataArray.push(data);
this.notifyDataAdd(this.dataArray.length - 1);
}
}
这里面只用到了添加方法,固其他方法省略,只保留基本的方法。
// WaterflowDemo.ets
import {
WaterFlowDataSource } from './WaterFlowDataSource'
@Entry
@Component
struct WaterflowDemo {
@State minSize: number = 80
@State maxSize: number = 280
@State colors: number[] = [0xFFC0CB, 0xDA70D6, 0x6B8E23, 0x6A5ACD, 0x00FFFF, 0x00FF7F]
datasource: WaterFlowDataSource = new WaterFlowDataSource()
private itemWidthArray: number[] = []
private itemHeightArray: number[] = []
// 计算flow item宽/高
getSize() {
let ret = Math.floor(Math.random() * this.maxSize)
return (ret > this.minSize ? ret : this.minSize)
}
// 新增数据,并保存flow item宽/高
getItemSizeArray(t=0) {
for (let i = 0; i < 20; i++) {
this.itemWidthArray.push(this.getSize())
this.itemHeightArray.push(this.getSize())
this.datasource.pushData(i+t)
}
}
aboutToAppear() {
this.getItemSizeArray()
}
build() {
Column() {
WaterFlow() {
LazyForEach(this.datasource, (item: number) => {
FlowItem() {
Column() {
Text("N" + item).fontSize(14)
}
}
.width(this.itemWidthArray[item])
.height(this.itemHeightArray[item])
.backgroundColor(this.colors[item % 5])
// 此处通过在FlowItem的onAppear中判断距离数据终点的数量,提前增加数据的方式实现了无停顿的无限滚动。
.onAppear(() => {
// 即将触底时提前增加数据
if (item + 20 == this.datasource.totalCount()) {
this.getItemSizeArray(this.datasource.totalCount())
}
})
}, item => item)
}
.columnsTemplate("1fr 1fr")
.columnsGap(10)
.rowsGap(10)
// 瀑布流组件到达起始位置时触发。
.onReachStart(() => {
console.info("onReachStart")
})
// 瀑布流组件到底末尾位置时触发。
.onReachEnd(() => {
console.info("onReachEnd")
})
.backgroundColor(0xFAEEE0)
.width('100%')
.height('100%')
}
}
}
这里没有图片,用了随机背景色代替。