鸿蒙 HarmonyOS 6 | ArkUI (04):数据展示 List 列表容器 LazyForEach 懒加载机制

文章目录

      • 前言
      • [一、 走出全量渲染的舒适区与性能陷阱](#一、 走出全量渲染的舒适区与性能陷阱)
      • [二、 LazyForEach 的按需渲染哲学与数据契约](#二、 LazyForEach 的按需渲染哲学与数据契约)
      • [三、 键值生成与缓存策略的博弈](#三、 键值生成与缓存策略的博弈)
      • [四、 实战](#四、 实战)
      • 总结

前言

回想一下我们每天使用手机的场景,无论是清晨浏览新闻资讯,午休时刷短视频,还是睡前查看电商平台的购物订单,这些海量信息的呈现方式无一例外都是列表。对于用户而言,手指在屏幕上滑动的流畅度直接决定了对一款应用的第一印象,哪怕出现几毫秒的掉帧或者瞬间的白屏,都可能让用户心生退意。而对于我们开发者来说,构建一个能跑通的列表界面似乎是入门必修课,甚至在很多初级教程中,只需要几行简单的代码就能把数组里的数据渲染到屏幕上。

但是,当我们把数据量从几十条增加到一千条、一万条时,那个曾经丝般顺滑的界面可能会突然变得卡顿、手机发烫,甚至因为内存溢出而直接闪退。这就是初级工程师与资深开发者的分水岭所在。

在鸿蒙 HarmonyOS 6 的开发里,掌握 List 列表容器仅仅是起点,而真正能让我们驾驭海量数据、实现极致性能体验的核心钥匙,在于理解并精通 LazyForEach 懒加载机制。

一、 走出全量渲染的舒适区与性能陷阱

在 ArkUI 的组件体系中,创建一个列表是极其符合直觉的。我们通常会使用 List 容器组件,它就像是一个能够滚动的长条盒子,而在盒子内部,我们通过 ListItem 来承载具体的每一行内容。对于刚接触鸿蒙开发的同学来说,最顺手的工具肯定是 ForEach 循环渲染。它的逻辑非常简单直接,我们给它一个数组,它就老老实实地遍历数组中的每一个元素,然后为每一个元素创建一个对应的组件。这种全量渲染的模式在数据量较少时,比如只有二三十条设置项,是完全没有问题的,代码写起来也清晰易懂。

复制代码
// 1. 数据源
@State dataList: string[] = ['核心概念', '组件通信', '路由管理', '状态管理'];

build() {
  // 2. List 容器:类似滚动的长条盒子
  List({ space: 12 }) { 
    // 3. ForEach:循环渲染
    // 参数1:数据源
    // 参数2:组件生成函数
    // 参数3:键值生成函数 (性能关键,用于唯一标识)
    ForEach(this.dataList, (item: string) => {
      
      // 4. ListItem:承载具体的每一行
      ListItem() {
        Text(item)
          .fontSize(16)
          .width('100%')
          .padding(15)
          .backgroundColor(Color.White)
          .borderRadius(10)
      }
      
    }, (item: string) => item) // 唯一 Key,避免不必要的重新渲染
  }
  .width('100%')
  .height('100%')
  .padding(16)
}

我们必须警惕这种舒适区往往也是性能的陷阱。ForEach 的工作机制决定了它会一次性加载所有的数据。

如果服务器给我们返回了一万条历史订单数据,如果我们直接使用 ForEach 进行渲染,ArkUI 就会尝试在瞬间创建一万个 ListItem 组件以及它们内部的所有子组件。这不仅会瞬间占满应用的内存,大量的布局计算和节点创建任务还会死死地堵塞主线程,导致用户看到页面长时间的白屏或者严重的掉帧。这就是为什么很多新手的应用在测试阶段数据少时跑得飞快,一上线遇到真实数据就崩溃的原因。

我们必须意识到,屏幕的显示区域是有限的,用户同一时间能看到的可能只有五六条数据,为那些还未出现在屏幕上的九千多条数据提前创建组件,是一种极大的资源浪费。

二、 LazyForEach 的按需渲染哲学与数据契约

为了解决全量渲染带来的性能灾难,HarmonyOS 引入了 LazyForEach 组件。

它的名字非常直观,Lazy 代表懒惰,但在计算机科学中,这里的懒惰意味着极致的高效。LazyForEach 的核心哲学是 按需渲染 。它只会为当前屏幕可见区域以及可视区域附近少量的预加载区域创建组件。当用户向上滑动屏幕时,下方的列表项即将进入屏幕,LazyForEach 才会向数据源请求数据并创建新的组件;而当上方的列表项滑出屏幕并远离可视区域时,它们所占用的组件资源会被销毁或者回收进入复用池。这种机制就像是一个滑动的窗口,无论我们的底层数据有多少万条,内存中实际存在的组件数量始终维持在一个很小的、稳定的范围内。

这种高性能是有门槛的。与 ForEach 直接接收一个简单的数组不同,LazyForEach 要求我们提供一个实现了 IDataSource 接口的数据源对象。这对于很多习惯了直接操作数组的前辈来说,可能是一个思维上的转变。在懒加载的模式下,ArkUI 框架不再直接持有数据的所有权,它变成了一个单纯的索取者。它会不断地问我们:总共有多少条数据?第 5 条数据是什么?作为开发者,我们需要构建一个能够回答这些问题的数据管理代理。

在实际的工程实践中,我们绝不会在每一个页面里都去手写一遍 IDataSource 的实现逻辑。那样不仅代码冗余,而且极易出错。成熟的做法是封装一个 BasicDataSource 基类。这样做的好处是,我们可以把那些枯燥的监听器管理代码、数据的增删改查通知逻辑全部封装起来,在具体的业务代码中,我们只需要关注数据的获取本身。这不仅让代码更加整洁,也符合面向对象编程的复用原则。

我们可以看看下面这个通用的基类封装,它是我们构建高性能列表的基石。

复制代码
// BasicDataSource.ets - 通用数据源基类
class BasicDataSource<T> implements IDataSource {
  private listeners: DataChangeListener[] = [];
  private originDataArray: T[] = [];

  // 告诉框架总共有多少条数据
  totalCount(): number {
    return this.originDataArray.length;
  }

  // 告诉框架指定索引的数据是什么
  getData(index: number): T {
    return this.originDataArray[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);
    }
  }

  // 初始化或重置数据
  public setData(data: T[]) {
    this.originDataArray = data;
    this.notifyDataReload();
  }

  // 通知所有监听器:数据重载了
  notifyDataReload(): void {
    this.listeners.forEach(listener => {
      listener.onDataReloaded();
    });
  }
}

三、 键值生成与缓存策略的博弈

当我们封装好了数据源基类后,使用 LazyForEach 时还有两个技术细节决定了最终的成败:一个是键值生成规则,一个是缓存数量。LazyForEach 的第三个参数是 keyGenerator ,它的作用是为每一个数据项生成一个唯一的身份证。很多开发者容易忽视这一点,甚至为了省事直接使用数组的 index 索引作为 Key。这在列表内容静态不变时或许能侥幸过关,可一旦涉及到数据的插入或删除,就会出问题。

因为当我们删除列表头部的元素时,后面所有元素的索引都会发生变化,这会导致框架误判所有组件都需要更新,从而触发全量的销毁和重建,让懒加载的复用机制彻底失效。正确的做法是永远使用数据对象中本身具备的唯一标识,比如用户 ID 或者订单号。这样无论数据如何在数组中移动,框架都能通过这个唯一的 Key 识别出它,从而复用已经存在的 UI 组件。

除了 Key,cachedCount 属性则是调节性能与体验的杠杆。它控制着列表的预加载数量。默认情况下,LazyForEach 只加载屏幕内的项目。但这会带来一个问题,如果用户滑动得非常快,新的列表项还没来得及渲染,屏幕边缘就会出现短暂的白块。我们可以设置 cachedCount,比如将其设置为 5,意味着框架会在屏幕可视区域的上下方额外预先渲染 5 个列表项。这样当用户滑动时,内容已经准备好了,体验就会非常丝滑。但这个数值也不是越大越好,过大的缓存数量又会重新带来内存压力,我们需要在流畅度和内存占用之间找到一个平衡点。

四、 实战

为了让大家更直观地理解这些概念如何协同工作,我们来构建一个完整的新闻列表场景。这个示例代码不仅包含了一个继承自泛型基类的具体业务数据源,还演示了如何在 List 组件中正确配置 LazyForEachcachedCount。你可以直接将这段代码复制到你的项目中,它能够毫无压力地处理上千条数据的渲染。

复制代码
import { promptAction } from '@kit.ArkUI';

// 1. 定义数据模型
// 在实际项目中,这里通常对应后端 API 返回的 JSON 结构
class NewsData {
  id: string;
  title: string;
  summary: string;
  timestamp: string;

  constructor(id: string, title: string, summary: string) {
    this.id = id;
    this.title = title;
    this.summary = summary;
    this.timestamp = new Date().toLocaleTimeString();
  }
}

// 2. 引入我们之前定义的通用数据源基类
// (为了代码的完整性,这里再次展示简化版,实际开发中请抽离为单独文件)
class BasicDataSource<T> implements IDataSource {
  private listeners: DataChangeListener[] = [];
  private originDataArray: T[] = [];

  totalCount(): number {
    return this.originDataArray.length;
  }
  getData(index: number): T {
    return this.originDataArray[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);
    }
  }
  public setData(data: T[]) {
    this.originDataArray = data;
    this.notifyDataReload();
  }
  notifyDataReload(): void {
    this.listeners.forEach(listener => {
      listener.onDataReloaded();
    });
  }
}

// 3. 具体的业务数据源
class NewsDataSource extends BasicDataSource<NewsData> {
}

@Entry
@Component
struct LazyListPerformancePage {
  // 实例化我们的数据源对象
  private newsDataSource: NewsDataSource = new NewsDataSource();
  
  // 模拟生成数据的辅助函数
  private generateMockData(count: number): NewsData[] {
    let dataList: NewsData[] = [];
    for (let i = 0; i < count; i++) {
      const id = i.toString();
      dataList.push(new NewsData(
        id, 
        `鸿蒙 HarmonyOS 6 高性能新闻标题 #${id}`, 
        `这是第 ${i} 条新闻的详细摘要。我们正在使用 LazyForEach 技术来确保列表滑动的极致流畅。`
      ));
    }
    return dataList;
  }

  // 页面即将显示时加载数据
  aboutToAppear(): void {
    // 模拟加载 1000 条数据
    const mockData = this.generateMockData(1000);
    this.newsDataSource.setData(mockData);
  }

  build() {
    Column() {
      // 顶部标题栏
      Text('高性能资讯流')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .width('100%')
        .padding(20)
        .backgroundColor('#F1F3F5')

      // List 容器开始
      List({ space: 12 }) {
        // 核心:使用 LazyForEach 替代 ForEach
        LazyForEach(this.newsDataSource, (item: NewsData) => {
          ListItem() {
            // 列表项的具体布局
            Column({ space: 8 }) {
              Row() {
                Text(item.title)
                  .fontSize(16)
                  .fontWeight(FontWeight.Medium)
                  .maxLines(1)
                  .layoutWeight(1)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                
                Text(item.timestamp)
                  .fontSize(12)
                  .fontColor('#999999')
              }
              .width('100%')
              .justifyContent(FlexAlign.SpaceBetween)

              Text(item.summary)
                .fontSize(14)
                .fontColor('#666666')
                .maxLines(2)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
                .lineHeight(20)
            }
            .width('100%')
            .padding(16)
            .backgroundColor(Color.White)
            .borderRadius(12)
            .shadow({ radius: 4, color: '#1A000000', offsetY: 2 })
          }
          .onClick(() => {
            promptAction.showToast({ message: `点击了新闻 ID: ${item.id}` });
          })
        }, (item: NewsData) => item.id) // 关键点:使用唯一的 id 作为 Key
      }
      .width('100%')
      .layoutWeight(1) // 让列表占据剩余的所有高度
      .cachedCount(4)  // 关键点:预加载屏幕外的 4 项,防止快速滑动白块
      .padding({ left: 16, right: 16, bottom: 16 })
      .divider({ strokeWidth: 0 }) // 隐藏默认分割线
      .scrollBar(BarState.Off)     // 隐藏滚动条让视觉更清爽
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F1F3F5')
  }
}

总结

回顾我们探讨的内容,从简单的 ForEach 到高性能的 LazyForEach,这不仅仅是 API 的更换,更是一种开发思维的进阶。

我们学会了如何通过 IDataSource 建立数据与视图的契约,如何通过 cachedCount 平衡内存与流畅度,以及如何利用稳定的 Key 来榨干框架的复用能力。

在鸿蒙 HarmonyOS 6 的全栈开发中,列表性能优化是衡量一个应用质量的基石。一个能够流畅加载万级数据的列表,往往比花哨的动画更能赢得用户的信任。

相关推荐
sam.li11 小时前
鸿蒙HAR对外发布安全流程
安全·华为·harmonyos
sam.li12 小时前
鸿蒙APP安全体系
安全·华为·harmonyos
福楠13 小时前
C++ STL | list
c语言·开发语言·数据结构·c++·算法·list
福楠13 小时前
模拟实现list容器
c语言·开发语言·数据结构·c++·list
ChinaDragon14 小时前
HarmonyOS:通过组件导航设置自定义区域
harmonyos
人工智能知识库15 小时前
华为HCIP-HarmonyOS Application Developer题库 H14-231 (26年最新带解析)
华为·harmonyos·hcip-harmonyos·h14-231
C雨后彩虹15 小时前
亲子游戏问题
java·数据结构·算法·华为·面试
以太浮标15 小时前
华为eNSP模拟器综合实验之- 端口镜像(Port Mirroring)配置解析
运维·服务器·网络·华为
搬砖的kk15 小时前
鸿蒙 PC 版 DevEco Studio 使用 OHPM 下载三方库教程
华为·harmonyos
以太浮标1 天前
华为eNSP模拟器综合实验之-DHCP服务中继配置案例
网络·华为·智能路由器·信息与通信