Harmony os LazyForEach:数据懒加载详解

LazyForEach:数据懒加载详解

鸿蒙第四期活动

1. 概念与原理

LazyForEach 从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。

当 LazyForEach 放在 可滚动容器 中(List / Grid / Swiper / WaterFlow),框架会根据可视区域按需创建组件;当组件滚出可视区域后,会销毁并回收,降低内存占用。

可以理解为:

  • 你准备一份数据源(数组 / 数据源类)
  • LazyForEach 根据数据源"虚拟地"认为列表有多少项
  • 当前屏幕只渲染能看到的那一部分,其余的在需要时才创建

2. 使用优势

2.1 性能优势

  • 按需加载
    • 只渲染当前可视区域内的项目
    • 滚动到哪儿再加载哪儿,避免一次性创建大量组件
  • 组件复用
    • 滚动时复用同一批组件实例,仅更新其中的内容数据
    • 类似原生的「Recycler」机制(RecyclerView/ListView 的思想)

2.2 使用场景

  • 列表数据较多:商品列表、聊天记录、微博流、新闻流
  • 网格数据较多:图片墙、相册、瀑布流
  • 轮播页大量页面:Swiper / Banner

3. 使用说明与关键点

3.1 必须放在支持懒加载的容器中

目前支持 LazyForEach 的容器:

  • List
  • Grid
  • Swiper
  • WaterFlow(瀑布流)

❗ LazyForEach 不能 单独使用,必须被上述容器包裹。


3.2 键函数(keyGenerator)非常重要

LazyForEach 的函数签名一般是:

ts 复制代码
LazyForEach(
  dataSource,                // 数据源:数组 / 数据源类
  (item) => { /* 生成组件 */ },
  (item) => key              // keyGenerator:返回唯一 key
)

这里的 key 要满足:

  1. 稳定:同一条数据的 key 不应在刷新/滚动中改变
  2. 唯一:同一个 LazyForEach 中每条数据的 key 不能重复

推荐写法:使用真实业务 ID,比如接口返回的 iduuid,而不是数组下标。


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 这个例子体现的点

  1. LazyForEach + Grid 可以很方便地构建大规模网格布局(商品墙、图片墙等);
  2. onReachEnd + loadMore 实现简单的分页加载;
  3. cachedCount 同时用在 List 和 Grid 上,保证滚动时比较流畅。

6. 开发中的一些建议与踩坑

  1. key 一定要稳定且唯一
    • 优先使用业务 ID(例如后端返回的 id
    • 不要用数组下标,尤其是会插入/删除元素的列表
  2. 列表项组件尽量简单
    • 把复杂逻辑拆分成子组件,避免一个列表项组件过于臃肿
    • 配合 @Reusable,让复用更高效
  3. 大批量更新要谨慎
    • 频繁对数据源做单条增删改会触发大量刷新
    • 对于大数据,更推荐"批量修改后一次性替换数组"的方式,或自己封装数据源类统一通知
  4. 合理设置 cachedCount
    • 如果滑动时出现短暂白屏,可以适当调大 cachedCount
    • 但也不要盲目拉满,过大可能占用更多内存
  5. 优先用 LazyForEach
    • 当数据量超过几十 / 上百条时,尽量不要用普通 ForEach
    • List / Grid + LazyForEach 是 ArkUI 列表的「标准组合」
相关推荐
MrTan1 小时前
Uni-App 鸿蒙应用微信相关功能上架踩坑:自制微信安装检测插件
uni-app·harmonyos
繁华似锦respect1 小时前
C++ 无锁队列(Lock-Free Queue)详细介绍
linux·开发语言·c++·windows·visual studio
qq_433192181 小时前
Linux ISCSI服务器配置
linux·服务器·数据库
Dest1ny-安全1 小时前
CTF入门:国内线上CTF比赛时间及部分题目资源
网络·安全·web安全·微信小程序·php
在路上看风景1 小时前
7.2 认证和报文的完整性
网络
python百炼成钢1 小时前
47.Linux UART 驱动
linux·运维·服务器·驱动开发
sonadorje1 小时前
HTTP Cookie解析
网络·网络协议·http
w***48821 小时前
【MySQL】视图、用户和权限管理
android·网络·mysql
little_kid_pea1 小时前
Oracle:从收费明细中扣减退费数据
java·服务器·数据库