LazyForEach:数据懒加载详解
1. 概念与原理
LazyForEach 从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。
当 LazyForEach 放在 可滚动容器 中(List / Grid / Swiper / WaterFlow),框架会根据可视区域按需创建组件;当组件滚出可视区域后,会销毁并回收,降低内存占用。
可以理解为:
- 你准备一份数据源(数组 / 数据源类)
- LazyForEach 根据数据源"虚拟地"认为列表有多少项
- 当前屏幕只渲染能看到的那一部分,其余的在需要时才创建
2. 使用优势
2.1 性能优势
- 按需加载
- 只渲染当前可视区域内的项目
- 滚动到哪儿再加载哪儿,避免一次性创建大量组件
- 组件复用
- 滚动时复用同一批组件实例,仅更新其中的内容数据
- 类似原生的「Recycler」机制(RecyclerView/ListView 的思想)
2.2 使用场景
- 列表数据较多:商品列表、聊天记录、微博流、新闻流
- 网格数据较多:图片墙、相册、瀑布流
- 轮播页大量页面:Swiper / Banner
3. 使用说明与关键点
3.1 必须放在支持懒加载的容器中
目前支持 LazyForEach 的容器:
ListGridSwiperWaterFlow(瀑布流)
❗ LazyForEach 不能 单独使用,必须被上述容器包裹。
3.2 键函数(keyGenerator)非常重要
LazyForEach 的函数签名一般是:
ts
LazyForEach(
dataSource, // 数据源:数组 / 数据源类
(item) => { /* 生成组件 */ },
(item) => key // keyGenerator:返回唯一 key
)
这里的 key 要满足:
- 稳定:同一条数据的 key 不应在刷新/滚动中改变
- 唯一:同一个 LazyForEach 中每条数据的 key 不能重复
推荐写法:使用真实业务 ID,比如接口返回的 id、uuid,而不是数组下标。
3.3 可复用组件(@Reusable)
-
给列表项组件加上
@Reusable装饰器,可以显式告诉系统:"这玩意儿可以被复用"
-
系统会在滚动时复用组件实例,只更新绑定的数据,进一步提升性能。
3.4 cachedCount:预加载数量
容器(List/Grid/WaterFlow)提供 cachedCount 属性,用于设置前后预加载数量:
ts
List() {
LazyForEach(this.dataList, ... )
}
.cachedCount(10) // 在可视区域前后各缓存 10 项
适当调大可以减少滑动时的白屏 / 卡顿,但太大会增加内存占用。
4. 基础示例:简单列表 + LazyForEach
目标:
- 使用 LazyForEach 在 List 中展示 100 条"项目 x";
- 使用可复用组件
ListItemComponent;- 支持后续增删改。
4.1 完整代码
ts
// 1. 定义数据类型
class ItemData {
id: string // 唯一 ID
name: string
constructor(id: string, name: string) {
this.id = id
this.name = name
}
}
// 2. 列表项组件(可复用)
@Reusable // 标记组件可复用
@Component
struct ListItemComponent {
@Prop item: ItemData // 接收单条数据
build() {
Row() {
Text(this.item.name)
.fontSize(20)
.margin(10)
}
.width('100%')
}
}
// 3. 主页面
@Entry
@Component
struct MyPage {
@State private dataList: ItemData[] = [] // 数据源(加 @State,UI 才会随数据变化)
aboutToAppear() {
// 初始化 100 条数据(真实场景一般从网络请求)
for (let i = 0; i < 100; i++) {
this.dataList.push(new ItemData(i.toString(), `项目 ${i}`))
}
}
build() {
List() {
LazyForEach(
this.dataList, // 数据源:数组
(item: ItemData) => { // 渲染每个项目
ListItem() {
ListItemComponent({ item: item })// 使用我们定义的子组件
}
},
(item: ItemData) => item.id // 唯一 key:使用真实 id
)
}
.width('100%')
.height('100%')
.cachedCount(10) // 预加载前后各 10 项
}
// ===== 三种常见操作 =====
// 添加数据
private addItem() {
const newId = (this.dataList.length + 1).toString()
this.dataList.push(new ItemData(newId, `新项目 ${newId}`))
// 使用 @State 数组时,push 会触发 UI 更新,无需额外 notify
}
// 删除某个 id 对应的数据
private deleteItemById(targetId: string) {
const index = this.dataList.findIndex(item => item.id === targetId)
if (index >= 0) {
this.dataList.splice(index, 1)
}
}
// 更新某个 id 对应的数据
private updateItemById(targetId: string, newName: string) {
const index = this.dataList.findIndex(item => item.id === targetId)
if (index >= 0) {
this.dataList[index].name = newName
// 这里修改的是对象内部字段,必要时可以重新赋值给数组触发刷新:
this.dataList = [...this.dataList]
}
}
}
4.2 小结
dataList用@State修饰,数组变化会触发组件刷新;LazyForEach+keyGenerator负责列表渲染和复用;@Reusable+ 简单的子组件结构,使得复用更加高效。
5. 实战示例:商品网格 Grid + LazyForEach + 上拉加载更多
目标:
做一个简单的"商品九宫格",支持:
- 使用 Grid + LazyForEach 渲染;
- 滚动到列表底部时自动加载更多(模拟分页);
- 使用 cachedCount 预加载。
5.1 数据模型与卡片组件
ts
class GoodsItem {
id: string
title: string
price: string
constructor(id: string, title: string, price: string) {
this.id = id
this.title = title
this.price = price
}
}
@Reusable
@Component
struct GoodsCard {
@Prop item: GoodsItem
build() {
Column() {
// 假装有商品封面图
Rect() // 这里用一个矩形代替图片
.width('100%')
.height(80)
.fill(Color.Gray)
.borderRadius(8)
Text(this.item.title)
.fontSize(14)
.maxLines(1)
.margin({ top: 4 })
Text(`¥${this.item.price}`)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.margin({ top: 2 })
}
.padding(8)
.backgroundColor('#FFFFFF')
.borderRadius(12)
}
}
5.2 主页面:Grid + LazyForEach
ts
@Entry
@Component
struct GoodsPage {
@State private goodsList: GoodsItem[] = []
@State private page: number = 1
private pageSize: number = 20
aboutToAppear() {
this.loadMore() // 初次进入加载第一页
}
private loadMore() {
// 模拟网络加载:一次加载 pageSize 条数据
const start = (this.page - 1) * this.pageSize
for (let i = 0; i < this.pageSize; i++) {
const id = (start + i).toString()
this.goodsList.push(
new GoodsItem(id, `商品 ${id}`, (Math.random() * 100).toFixed(2))
)
}
this.page++
}
build() {
// 外层用 List 做整体滚动容器,内部用 Grid 做网格布局
List() {
ListItem() {
Grid() {
LazyForEach(
this.goodsList,
(item: GoodsItem) => {
GridItem() {
GoodsCard({ item: item })
}
},
(item: GoodsItem) => item.id
)
}
.columnsTemplate('1fr 1fr') // 两列网格
.rowsGap(12)
.columnsGap(12)
.padding(12)
.cachedCount(6) // 网格预加载
}
}
.onReachEnd(() => { // 滑到列表底部触发加载更多
this.loadMore()
})
.cachedCount(2) // List 本身也预加载 2 个 ListItem
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
}
5.3 这个例子体现的点
- LazyForEach + Grid 可以很方便地构建大规模网格布局(商品墙、图片墙等);
onReachEnd+loadMore实现简单的分页加载;cachedCount同时用在 List 和 Grid 上,保证滚动时比较流畅。
6. 开发中的一些建议与踩坑
- key 一定要稳定且唯一
- 优先使用业务 ID(例如后端返回的
id) - 不要用数组下标,尤其是会插入/删除元素的列表
- 优先使用业务 ID(例如后端返回的
- 列表项组件尽量简单
- 把复杂逻辑拆分成子组件,避免一个列表项组件过于臃肿
- 配合
@Reusable,让复用更高效
- 大批量更新要谨慎
- 频繁对数据源做单条增删改会触发大量刷新
- 对于大数据,更推荐"批量修改后一次性替换数组"的方式,或自己封装数据源类统一通知
- 合理设置 cachedCount
- 如果滑动时出现短暂白屏,可以适当调大 cachedCount
- 但也不要盲目拉满,过大可能占用更多内存
- 优先用 LazyForEach
- 当数据量超过几十 / 上百条时,尽量不要用普通 ForEach
- List / Grid + LazyForEach 是 ArkUI 列表的「标准组合」