前言

在鸿蒙 ArkTS 应用开发中,列表(List)是最高频使用的组件之一。无论是新闻流、商品列表还是聊天记录,都离不开列表渲染。ArkTS 提供了 ForEach 和 LazyForEach 两种循环渲染接口。虽然两者在简单场景下表现相似,但在处理大数据量时,其性能差异巨大。

一、 核心原理对比

1. ForEach:全量渲染机制
ForEach 采用的是“一次性全量渲染”策略。当页面加载时,它会遍历数据源中的所有数据,为每一个数据项创建对应的组件节点,并一次性挂载到组件树上。

  • 工作机制:数据源有多少条,UI 树就生成多少个节点。
  • 适用场景:数据量较小(通常建议少于 100 条)且数据相对固定的静态列表。

2. LazyForEach:按需懒加载机制
LazyForEach 采用的是“按需懒加载”策略。它仅渲染当前屏幕可视区域内(以及预加载区域)的组件。当用户滑动列表时,滑出屏幕的组件会被销毁回收,滑入屏幕的新数据才会触发组件创建。

  • 工作机制:UI 节点数量仅与屏幕可见区域大小相关,与数据总量无关。
  • 适用场景:数据量巨大(成百上千条)、需要无限滚动加载的动态长列表。
二、 性能实测数据

为了直观展示两者的差异,我们基于 DevEco Studio 的 Profiler 工具,在相同硬件环境下对 10,000 条数据 的列表进行压力测试。

性能指标 ForEach (全量渲染) LazyForEach (懒加载) 性能提升
列表挂载耗时 3291 ms 97 ms 33倍
完全显示耗时 5841 ms 1707 ms 3.4倍
独占内存占用 560.1 MB 82.9 MB 节省 85%
滑动丢帧率 58.2% (严重卡顿) 6.6% (流畅) 体验质变

结论:在大数据量场景下,ForEach 会导致严重的内存膨胀和界面卡顿,而 LazyForEach 能保持极低的内存占用和流畅的滑动体验。

三、 代码实战与实现

1. ForEach 基础实现
ForEach 的使用非常简洁,直接在 build 方法中遍历数组即可。

@Entry
@Component
struct ForEachExample {
  // 简单的静态数据
  private items: string[] = ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5'];

  build() {
    List({ space: 10 }) {
      // 参数1:数据源
      // 参数2: item生成函数
      // 参数3: key生成函数 (必须唯一)
      ForEach(this.items, (item: string) => {
        ListItem() {
          Text(item)
            .fontSize(20)
            .height(60)
            .width('100%')
            .backgroundColor(Color.Blue)
            .textAlign(TextAlign.Center)
        }
      }, (item: string) => item) 
    }
    .width('100%')
    .height('100%')
  }
}

2. LazyForEach 进阶实现
LazyForEach 要求数据源必须实现 IDataSource 接口,以便框架能够监听数据变化并动态加载。

import promptAction from '@ohos.promptAction';

// 1. 实现 IDataSource 接口
class MyDataSource implements IDataSource {
  // 将 private 改为 public,允许外部直接访问和操作 list 数组
  public list: string[] = [];
  private listener: DataChangeListener | undefined;

  constructor(list: string[]) {
    this.list = list;
  }

  // 获取数据总数
  totalCount(): number {
    return this.list.length;
  }

  // 获取指定索引的数据
  getData(index: number): string {
    return this.list[index];
  }

  // 注册数据变化监听器
  registerDataChangeListener(listener: DataChangeListener): void {
    this.listener = listener;
  }

  // 注销数据变化监听器
  unregisterDataChangeListener(listener: DataChangeListener): void {
    this.listener = undefined;
  }

  // 通知数据重载(通常在数据增加后调用)
  notifyDataReload(): void {
    this.listener?.onDataReloaded();
  }
}

@Entry
@Component
struct LazyForEachExample {
  // 初始化数据源
  private dataSource: MyDataSource = new MyDataSource([]);

  aboutToAppear() {
    // 模拟初始化 50 条数据
    for (let i = 0; i < 50; i++) {
      this.dataSource.list.push(`Lazy Item ${i}`);
    }
  }

  build() {
    List({ space: 10 }) {
      // 使用 LazyForEach 替代 ForEach
      LazyForEach(this.dataSource, (item: string) => {
        ListItem() {
          Text(item)
            .fontSize(20)
            .height(60)
            .width('100%')
            .backgroundColor(Color.Green)
            .fontColor(Color.White)
            .textAlign(TextAlign.Center)
        }
      }, (item: string) => item) // 使用字符串内容作为唯一键值
    }
    .width('100%')
    .height('100%')
    .onReachEnd(() => {
      // 触底加载更多数据
      for (let i = 0; i < 10; i++) {
        this.dataSource.list.push(`New Item ${this.dataSource.list.length}`);
      }
      this.dataSource.notifyDataReload(); // 通知框架更新 UI
    })
  }
}
四、 避坑指南与最佳实践

1. 唯一键值(Key)的重要性
无论是 ForEach 还是 LazyForEach,第三个参数 keyGenerator 至关重要。

  • 错误做法:使用数组索引 (item, index) => index。当列表发生插入、删除操作时,会导致索引错位,引发 UI 渲染错乱。
  • 正确做法:使用数据中唯一的 ID (item) => item.id

2. 解决 LazyForEach 滑动白屏
在快速滑动长列表时,如果组件创建速度跟不上滑动速度,可能会出现短暂的白屏。

  • 解决方案:使用 .cachedCount() 属性。
List() {
  // ...
}
.cachedCount(5) // 提前预加载屏幕外 5 个组件,牺牲少量内存换取流畅度

3. 选型决策树

  • 数据量 < 100 条:优先使用 ForEach,开发效率高,代码简洁。
  • 数据量 > 1000 条:必须使用 LazyForEach,保证应用不崩溃、不卡顿。
  • 不确定数据量:建议默认使用 LazyForEach,这是一种防御性的编程习惯。
Logo

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

更多推荐