鸿蒙 ArkTS 长列表性能:ForEach 与 LazyForEach 怎么选

举报
yd_269937635 发表于 2026/11/09 16:37:55 2026/11/09
【摘要】 ForEach 会一次性创建所有子组件,长列表必然卡顿。LazyForEach 只创建可见区域并复用,代价是要实现 IDataSource。文中给出完整实现、数据变更通知写法,以及键生成器为什么不能省。

一、先看现象

鸿蒙应用里最常见的性能问题,是长列表滚动卡顿。一个几百条数据的 List,用 ForEach 渲染出来,滑动起来一顿一顿的,数据越多越明显。

多数人第一反应是"数据太多",于是做分页。但根因往往不在数据量,而在渲染方式。

二、ForEach 做了什么

```typescript List() { ForEach(this.items, (item: Item) => { ListItem() { ItemCard({ data: item }) } }, (item: Item) => item.id) } ```

`ForEach` 的语义是"把这个数组里的每一项都变成一个子组件"。它在构建阶段就一次性创建所有子组件——哪怕这个列表有一千条,屏幕只看得到十条,剩下九百九十条也全部创建了。

带来的后果:

  • 首屏构建慢,进入页面有明显顿挫;
  • 内存占用高,每个子组件都有自己的状态和布局树;
  • 数据变化时重建范围大,容易出现整列表闪烁。

分页能缓解,但只是把问题摊薄,没有解决"看不见的也在渲染"这件事。

三、LazyForEach 的机制

`LazyForEach` 只创建当前可见区域及其附近的子组件,滚出屏幕的会被回收复用。这正是长列表该有的行为。

它的用法不同:不能直接传数组,必须传一个实现了 `IDataSource` 接口的数据源对象。

```typescript List() { LazyForEach(this.dataSource, (item: Item) => { ListItem() { ItemCard({ data: item }) } }, (item: Item) => item.id) } ```

三个参数的含义和 `ForEach` 一致(数据源、子组件生成器、键生成器),区别只在第一个:数组换成了数据源。

四、IDataSource 怎么写

接口有四个必须实现的方法:

```typescript class BasicDataSource implements IDataSource { private listeners: DataChangeListener[] = [] private data: Item[] = []

totalCount(): number { return this.data.length }

getData(index: number): Item { return this.data[index] }

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) } }

// 以下是自己加的操作方法,改完要通知监听者 addItem(item: Item): void { this.data.push(item) this.listeners.forEach(l => l.onDataAdd(this.data.length - 1)) }

changeItem(index: number, item: Item): void { this.data[index] = item this.listeners.forEach(l => l.onDataChange(index)) }

deleteItem(index: number): void { this.data.splice(index, 1) this.listeners.forEach(l => l.onDataDelete(index)) }

reload(): void { this.listeners.forEach(l => l.onDataReloaded()) } } ```

关键在最后那四个方法:改完数据必须主动通知。`LazyForEach` 不会去监听数组本身,它只知道框架通过 `onDataAdd` / `onDataChange` / `onDataDelete` / `onDataReloaded` 告诉它的变化。

常见错误就是改了数据但忘了通知,界面不动,然后怀疑是 LazyForEach 不好用。

五、键生成器不能省

```typescript LazyForEach(this.dataSource, (item: Item) => { ListItem() { ItemCard({ data: item }) } }, (item: Item) => item.id) // ← 这个必须有 ```

键生成器返回每一项的唯一标识。框架靠它判断"这一项是不是同一项",从而决定复用哪个已回收的组件。

如果键不稳定(比如用数组下标 `index.toString()`),删除中间一项后,后面所有项的键都变了,框架会认为"全都变了",于是整段重建——性能退回到 ForEach 的水平,还可能引发状态错乱(复用了不该复用的组件)。

判断标准很简单:这个键在数据整个生命周期内是否始终指向同一条记录。用主键或 UUID,不要用下标。

六、ForEach 还有用吗

有。判断依据是列表长度和是否固定:

场景 建议
少于约 30 条、内容固定 `ForEach` 更简单,没必要上数据源
长列表、会滚动 `LazyForEach`
列表项高度差异极大 `LazyForEach` + 合理估算高度
需要频繁增删改 `LazyForEach`(靠通知精确更新,比整体重建便宜)

为几行数据写一套 `IDataSource` 是过度设计。但一旦列表会滚到屏幕外,收益就非常明显。

七、一个容易忽略的细节

`LazyForEach` 的 `itemGenerator` 会被多次调用——滚出去的项再滚回来,会重新执行这个函数。所以不要在生成器里做副作用(发请求、写日志、改外部状态),它应该是一个纯的"给数据渲染组件"的函数。

需要一次性初始化的工作,放在页面的 `aboutToAppear` 里,按数据 id 缓存在数据源对象上。

八、小结

两个 API 的区别可以概括成一句话:

  • `ForEach`:把所有数据都变成组件,简单但全量;
  • `LazyForEach`:只把看得见的数据变成组件,需要数据源,但可复用。

判断要不要换,就问一句:这个列表会滚出屏幕吗? 会,就该用 `LazyForEach`。

真正容易出错的地方不在 API 本身,而在两处细节:数据改了要通知、键要稳定。这两点做对,长列表的性能问题基本就消失了。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。