一张手绘笔记风格的框架图,主题是“HarmonyOS7 中 LazyForEach 适合什么场景”。

前言

LazyForEach 不是“更高级的 ForEach”。

如果页面只有十几个固定入口,普通 ForEach 更直接,代码也更好读。LazyForEach 适合的是另一类场景:数据量比较大,列表项比较重,并且数据源需要主动通知列表增删改。

我看到过一种误用:列表只有十来项,也硬套 LazyForEach,最后为了维护数据源写了一堆样板代码。页面没变快,代码先变复杂了。

一张手绘对比笔记图,主题是“普通 ForEach 和 LazyForEach 的区别”。用左右双栏对

性能优化要有场景,不要把每个列表都写成最复杂的版本。

为什么我不建议一上来就用它

LazyForEach 最容易被误用的地方,就是把它当成“更高级的 ForEach”。新手看到这名字,很容易下意识觉得它更专业、更适合大列表,于是页面明明只有十来条数据,也跟着套上去。

问题在于,LazyForEach 不是白送性能,它会把你带进数据源、监听通知、key 稳定性这些额外责任里。页面如果根本不需要这些能力,代码只会先复杂起来。

所以我更喜欢先问一句:这页到底是“普通渲染”还是“持续变化的长列表”。把这个问题想明白,再选组件就不容易跑偏。

一张手绘流程图,帮助判断什么时候才考虑使用 LazyForEach。流程从顶部开始:1)这是不是少量

什么时候才考虑 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,还要维护数据源协议。如果数据源通知写错,问题比普通数组更隐蔽。

放到真实项目里要注意什么

订单流水一般会来自分页接口,不会一次全拿。真实项目里可以在滚动到底部时请求下一页,再调用数据源的追加方法。到这一步时,你最好顺手把“是否还有下一页”“当前是否正在加载更多”“请求失败后怎么重试”这些状态一起设计好,不然后面很容易变成列表能滚、状态却接不上。

如果需要删除或更新某一行,数据源里也要提供对应方法,并调用类似 onDataDeleteonDataChange 的通知。只改数组不通知,是新手最常见的问题。很多人第一次接这类需求时,最容易卡住的就是“明明数据变了,为什么 UI 没反应”,根源通常就在这里。

还有一点:列表项不要依赖外部临时变量。比如用一个页面级变量记录“当前渲染到第几项”,这种写法在懒加载场景下很危险,因为列表项创建顺序不一定符合你的直觉。懒加载的核心不是“按顺序执行你写的每一行”,而是“列表按需要向数据源取数据”。

新手最容易踩的坑

第一个坑,是数据明明很少,也强行上 LazyForEach。结果列表性能没问题,自己先背上了一套数据源维护成本。

第二个坑,是只改数组,不发通知。比如你在 records.push() 之后忘了 onDataAdd,控制台看着像成功了,页面却没有按预期刷新,这种问题最容易把人绕晕。

第三个坑,是 key 写得不稳定。只要 key 不是唯一且长期稳定的,列表复用和刷新就可能出现奇怪现象,查起来还不直观。

放进真实项目还要补什么

真实项目里,订单流水通常还要补分页加载、下拉刷新、错误提示和空态处理。如果列表数据来自接口,你还要想清楚:下一页是直接追加,还是要先去重,再决定怎么通知数据源。

如果后面有“删除一条记录”“修改某一行状态”这样的需求,最好一开始就在数据源里把新增、删除、更新方法拆好。等功能做大了再补,通常会比一开始多绕一圈。

写在最后

你可以把 LazyForEach 理解成“给长列表准备的专业工具”,而不是“所有列表都该上的标准答案”。先判断场景,再决定复杂度,代码会更稳,你自己后面也会轻松很多。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐