HarmonyOS7 LazyForEach 适合什么场景:别把它当高级版 ForEach

文章目录
前言
LazyForEach 不是“更高级的 ForEach”。
如果页面只有十几个固定入口,普通 ForEach 更直接,代码也更好读。LazyForEach 适合的是另一类场景:数据量比较大,列表项比较重,并且数据源需要主动通知列表增删改。
我看到过一种误用:列表只有十来项,也硬套 LazyForEach,最后为了维护数据源写了一堆样板代码。页面没变快,代码先变复杂了。

性能优化要有场景,不要把每个列表都写成最复杂的版本。
为什么我不建议一上来就用它
LazyForEach 最容易被误用的地方,就是把它当成“更高级的 ForEach”。新手看到这名字,很容易下意识觉得它更专业、更适合大列表,于是页面明明只有十来条数据,也跟着套上去。
问题在于,LazyForEach 不是白送性能,它会把你带进数据源、监听通知、key 稳定性这些额外责任里。页面如果根本不需要这些能力,代码只会先复杂起来。
所以我更喜欢先问一句:这页到底是“普通渲染”还是“持续变化的长列表”。把这个问题想明白,再选组件就不容易跑偏。

什么时候才考虑 LazyForEach
可以先用这张表判断:
| 场景 | 更适合的写法 | 原因 |
|---|---|---|
| 少量固定入口 | ForEach |
简单、直观、维护成本低 |
| 上百条订单流水 | LazyForEach |
按需创建列表项,滚动更稳 |
| 聊天记录、文件列表 | LazyForEach |
数据可能持续追加或局部变化 |
| 频繁插入删除 | 看数据源封装 | 需要正确通知列表变化 |
这篇用订单流水做例子。订单记录会不断增加,列表项又有金额、标题、时间等字段,用 LazyForEach + IDataSource 演示比较合适。
先判断数据规模和更新方式,再决定是否使用
LazyForEach。为了炫技而上懒加载,通常会把简单页面写复杂。
先把列表变化方式想清楚
决定要不要上 LazyForEach 时,我通常先不看 API,而是先看这份列表会怎么变化。它是首屏一次性给完,还是会一页页追加;只是静态展示,还是会插入、删除、局部刷新;列表项是简单文本,还是一条条都带图片、金额、状态这些更重的内容。
像订单流水这种场景,我会先把“后面还会继续追加数据”“用户会来回滚动旧记录”“以后大概率要接分页接口”想明白。只要这几个条件开始出现,LazyForEach + IDataSource 就不再只是语法选择,而是在给后面的增量更新留位置。
对小白来说,最有价值的不是记住 LazyForEach 这个名字,而是养成先判断列表变化方式、再决定复杂度的习惯。
完整 ArkUI 示例
示例里有两个核心角色:OrderDataSource 管数据和通知,OrderLazyListPage 管页面展示和追加动作。
class OrderRecord {
id: number
title: string
amount: string
time: string
constructor(id: number, title: string, amount: string, time: string) {
this.id = id
this.title = title
this.amount = amount
this.time = time
}
}
class OrderDataSource implements IDataSource {
private records: OrderRecord[] = []
private listener: DataChangeListener | undefined
constructor(records: OrderRecord[]) {
this.records = records
}
totalCount(): number {
return this.records.length
}
getData(index: number): OrderRecord {
return this.records[index]
}
registerDataChangeListener(listener: DataChangeListener): void {
this.listener = listener
}
unregisterDataChangeListener(): void {
this.listener = undefined
}
push(record: OrderRecord): void {
this.records.push(record)
this.listener?.onDataAdd(this.records.length - 1)
}
}
@Entry
@Component
struct OrderLazyListPage {
private source: OrderDataSource = new OrderDataSource([
new OrderRecord(1, '购买开发者课程', '-¥99.00', '今天 10:20'),
new OrderRecord(2, '会员退款', '+¥19.00', '昨天 18:02'),
new OrderRecord(3, '模板市场收入', '+¥128.00', '周一 09:12')
])
private nextId: number = 4
build() {
Column({ space: 12 }) {
Row() {
Text('订单流水')
.fontSize(22)
.fontWeight(FontWeight.Bold)
Blank()
Button('追加一条')
.height(34)
.onClick(() => {
this.source.push(new OrderRecord(this.nextId, `新增流水 ${this.nextId}`, '-¥12.00', '刚刚'))
this.nextId += 1
})
}
.width('100%')
List({ space: 8 }) {
LazyForEach(this.source, (item: OrderRecord) => {
ListItem() {
Row() {
Column({ space: 5 }) {
Text(item.title).fontSize(16).fontWeight(FontWeight.Medium)
Text(item.time).fontSize(12).fontColor('#888888')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text(item.amount)
.fontSize(16)
.fontColor(item.amount.startsWith('+') ? '#00897B' : '#D32F2F')
}
.padding(14)
.backgroundColor(Color.White)
.borderRadius(10)
}
}, (item: OrderRecord) => item.id.toString())
}
.layoutWeight(1)
}
.padding(16)
.backgroundColor('#F5F7FA')
}
}
把关键代码一段段拆开
OrderDataSource implements IDataSource 是这篇代码的核心。LazyForEach 不是直接吃一个普通数组,而是通过数据源拿总数和指定位置的数据。
totalCount() 告诉列表当前有多少条记录,getData(index) 负责按索引取数据。它们看起来简单,但这是列表按需创建 item 的基础。
registerDataChangeListener() 保存列表传进来的监听器。后面数据变化时,数据源要通过这个监听器告诉 UI:哪里新增了,哪里删除了,哪里更新了。
push() 里最关键的是 this.listener?.onDataAdd(this.records.length - 1)。如果只 push 数组,不通知列表,数据源内部确实变了,但界面不一定知道要刷新。
LazyForEach 的 key 仍然要稳定。这里用订单 id,不要用金额或时间。金额可能重复,时间也可能重复,拿它们做 key 很容易埋坑。
和 ForEach 的区别别搞混
ForEach 更像是“我有一组数组,你帮我渲染出来”。它适合数据量不大、更新方式简单的页面。
LazyForEach 更像是“我有一个数据源,你按需向它拿数据,数据源变化时它会通知你”。它适合长列表、复杂列表、持续追加的列表。
这也是为什么 LazyForEach 的门槛更高:你不只是在写 UI,还要维护数据源协议。如果数据源通知写错,问题比普通数组更隐蔽。
放到真实项目里要注意什么
订单流水一般会来自分页接口,不会一次全拿。真实项目里可以在滚动到底部时请求下一页,再调用数据源的追加方法。到这一步时,你最好顺手把“是否还有下一页”“当前是否正在加载更多”“请求失败后怎么重试”这些状态一起设计好,不然后面很容易变成列表能滚、状态却接不上。
如果需要删除或更新某一行,数据源里也要提供对应方法,并调用类似 onDataDelete、onDataChange 的通知。只改数组不通知,是新手最常见的问题。很多人第一次接这类需求时,最容易卡住的就是“明明数据变了,为什么 UI 没反应”,根源通常就在这里。
还有一点:列表项不要依赖外部临时变量。比如用一个页面级变量记录“当前渲染到第几项”,这种写法在懒加载场景下很危险,因为列表项创建顺序不一定符合你的直觉。懒加载的核心不是“按顺序执行你写的每一行”,而是“列表按需要向数据源取数据”。
新手最容易踩的坑
第一个坑,是数据明明很少,也强行上 LazyForEach。结果列表性能没问题,自己先背上了一套数据源维护成本。
第二个坑,是只改数组,不发通知。比如你在 records.push() 之后忘了 onDataAdd,控制台看着像成功了,页面却没有按预期刷新,这种问题最容易把人绕晕。
第三个坑,是 key 写得不稳定。只要 key 不是唯一且长期稳定的,列表复用和刷新就可能出现奇怪现象,查起来还不直观。
放进真实项目还要补什么
真实项目里,订单流水通常还要补分页加载、下拉刷新、错误提示和空态处理。如果列表数据来自接口,你还要想清楚:下一页是直接追加,还是要先去重,再决定怎么通知数据源。
如果后面有“删除一条记录”“修改某一行状态”这样的需求,最好一开始就在数据源里把新增、删除、更新方法拆好。等功能做大了再补,通常会比一开始多绕一圈。
写在最后
你可以把 LazyForEach 理解成“给长列表准备的专业工具”,而不是“所有列表都该上的标准答案”。先判断场景,再决定复杂度,代码会更稳,你自己后面也会轻松很多。
更多推荐



所有评论(0)