鸿蒙学习实战之路-ArkTS数据懒加载_LazyForEach使用指南

害,上篇我们聊了ForEach循环渲染,有朋友私信问我:“西兰花啊,我有个列表要显示10000条数据,用ForEach直接卡死了,咋整?”

害,这个问题问得太好了!想象一下,你要在一个页面显示淘宝的所有商品,如果一次性渲染10000个商品组件,你的手机不得直接死机?就像做饭一样,谁也不可能同时炒10000道菜对吧~

今天这篇,我就手把手带你搞定ArkTS的数据懒加载LazyForEach,让你的应用在处理海量数据时依然流畅如丝!全程不超过15分钟~

什么是LazyForEach数据懒加载?

ArkUI通过自定义组件的build()函数和@Builder装饰器中的声明式UI描述语句构建相应的UI。在声明式描述语句中开发者除了使用系统组件外,还可以使用渲染控制语句来辅助UI的构建,这些渲染控制语句包括控制组件是否显示的条件渲染语句,基于数组数据快速生成组件的循环渲染语句,针对大数据量场景的数据懒加载语句,针对混合模式开发的组件渲染语句。

简单说,LazyForEach就是你的"智能上菜系统"!它不会一次性把所有菜都端上来,而是根据客人(用户)的需要,智能地上菜:看得见的先上,看不见的先等等~

适用场景:

  • 电商商品列表(海量商品展示)
  • 社交媒体动态流
  • 聊天消息列表
  • 新闻资讯列表
  • 图片/视频瀑布流

🥦 西兰花小贴士
LazyForEach就像你家的"智能冰箱",只会把正在吃的菜拿出来,其他的放在里面保鲜,既不占地方又不浪费~

LazyForEach的核心优势

LazyForEach为开发者提供了基于数据源渲染出一系列子组件的能力。具体而言,LazyForEach从数据源中按需迭代数据,并在每次迭代时创建相应组件。当在滚动容器中使用了LazyForEach,框架会根据滚动容器可视区域按需创建组件,当组件滑出可视区域外时,框架会销毁并回收组件以降低内存占用。

性能提升原理

  1. 按需渲染:只渲染用户看得见的部分
  2. 组件回收:滑出屏幕的组件会被销毁释放内存
  3. 预加载机制:提前渲染少量缓冲数据,保证滚动流畅
  4. 智能缓存:结合缓存列表项、动态预加载、组件复用等方法

🥦 西兰花警告
在大量子组件的场景下,LazyForEach与缓存列表项、动态预加载、组件复用等方法配合使用,可以进一步提升滑动帧率并降低应用内存占用。这就像做饭时要用合适的锅具一样,工具选对了,事半功倍!

LazyForEach的使用限制

LazyForEach必须在容器组件内使用,仅有ListListItemGroupGridSwiper以及WaterFlow组件支持数据懒加载。

容器组件支持情况

容器组件 是否支持懒加载 适用场景
List 垂直/水平列表,商品展示
Grid 网格布局,图片展示
Swiper 轮播图,广告展示
WaterFlow 瀑布流,社交动态
ListItemGroup 分组列表,通讯录

其他组件仍然是一次性加载所有的数据。支持数据懒加载的父组件根据自身及子组件的高度或宽度计算可视区域内需布局的子节点数量。

关键限制说明

  1. 容器内只能包含一个LazyForEach:以List为例,不建议同时包含ListItem、ForEach、LazyForEach
  2. 每个迭代只能创建一个子组件:LazyForEach的子组件生成函数有且只有一个根组件
  3. 键值生成器必须唯一:每个数据必须生成唯一的键值
  4. 必须使用DataChangeListener:重新赋值第一个参数dataSource会导致异常

🥦 西兰花警告
容器组件内只能包含一个LazyForEach,这就像一个锅里不能同时煮两种完全不同的菜,味道会串的!

基本用法详解

设置数据源

为了管理DataChangeListener监听器和通知LazyForEach更新数据,开发者需要使用如下方法:首先实现LazyForEach提供的IDataSource接口,将其作为LazyForEach的数据源,然后管理监听器和更新数据。

// 基础数据源类实现
class 菜品数据源 extends BasicDataSource {
  private 菜品列表: string[] = [];

  // 获取数据总数
  public totalCount(): number {
    return this.菜品列表.length;
  }

  // 根据索引获取数据
  public getData(index: number): string {
    return this.菜品列表[index];
  }

  // 添加数据
  public 添加菜品(菜品名: string): void {
    this.菜品列表.push(菜品名);
    this.notifyDataAdd(this.菜品列表.length - 1);
  }

  // 删除数据
  public 删除菜品(索引: number): void {
    this.菜品列表.splice(索引, 1);
    this.notifyDataDelete(索引);
  }
}

键值生成规则

在LazyForEach循环渲染过程中,系统为每个item生成一个唯一且持久的键值,用于标识对应的组件。键值变化时,ArkUI框架将视为该数组元素已被替换或修改,并基于新的键值创建新的组件。

LazyForEach提供了参数keyGenerator,开发者可以使用该函数生成自定义键值。如果未定义keyGenerator函数,ArkUI框架将使用默认的键值生成函数:

(item: Object, index: number) => { 
  return viewId + '-' + index.toString(); 
}

键值生成规则详解

键值应满足以下条件:

  • 唯一性:每个数据项对应的键值互不相同
  • 一致性:数据项不变时对应的键值也不变

上述条件保证LazyForEach正确、高效地更新子组件,否则可能存在渲染结果异常、渲染效率降低等问题。

首次渲染实战

@Entry
@Component
struct 菜品列表视图 {
  private 数据源: 菜品数据源 = new 菜品数据源();

  aboutToAppear() { 
    // 模拟添加21道菜品数据
    for (let i = 0; i <= 20; i++) {
      this.数据源.添加菜品(`经典菜品 ${i}`);
    }
  }

  build() { 
    List({ space: 3 }) {
      LazyForEach(this.数据源, (菜品: string) => {
        ListItem() {
          Row() {
            Text(菜品)
              .fontSize(18)
              .margin(10)
            }
            .padding(15)
            .borderRadius(8)
            .backgroundColor('#FFF8F9FA')
          }
          .onAppear(() => {
            console.info(`菜品显示: ${菜品}`);
          })
        }
      }, (菜品: string) => 菜品) // 使用菜品名作为唯一键值
    }
    .cachedCount(5) // 缓存5个项目用于预加载
  }
}

图1 LazyForEach正常首次渲染

在这里插入图片描述

在上述代码中,keyGenerator函数的返回值是菜品。LazyForEach循环渲染时,为数据源数组项依次生成键值"经典菜品 0"、“经典菜品 1”… “经典菜品 20”,并创建对应的ListItem子组件渲染到界面上。

错误案例:键值相同导致渲染异常

当不同数据项生成的键值相同时,框架的行为是不可预测的:

LazyForEach(this.数据源, (菜品: string) => {
  ListItem() {
    Row() {
      Text(菜品)
        .fontSize(18)
        .margin(10)
    }
    .padding(15)
    .borderRadius(8)
    .backgroundColor('#FFF8F9FA')
  }
}, (菜品: string) => '相同键值') // 自定义键值生成函数,返回相同键值
}
.cachedCount(5)

图2 LazyForEach存在相同键值

在这里插入图片描述

修改上述示例中LazyForEach的键值生成函数,使每个数据项生成唯一的键值:

}, (菜品: string, 索引: number) => `${菜品}-${索引}`) // 自定义键值生成函数,返回唯一键值

图3 LazyForEach生成唯一键值

在这里插入图片描述

数据更新操作

当LazyForEach数据源发生变化,需要再次渲染时,开发者应根据数据源的变化情况调用listener对应的接口,通知LazyForEach做相应的更新。LazyForEach的更新操作包括:添加数据、删除数据、交换数据、改变单个数据、改变多个数据以及精准批量修改数据。

添加数据操作

@Entry
@Component
struct 动态菜品列表 {
  private 数据源: 菜品数据源 = new 菜品数据源();

  aboutToAppear() { 
    for (let i = 0; i <= 20; i++) {
      this.数据源.添加菜品(`经典菜品 ${i}`);
    }
  }

  build() { 
    List({ space: 3 }) {
      LazyForEach(this.数据源, (菜品: string) => {
        ListItem() {
          Row() {
            Text(菜品)
              .fontSize(18)
              .margin(10)
          }
          .padding(15)
          .borderRadius(8)
          .backgroundColor('#FFF8F9FA')
        }
        .onClick(() => {
          // 点击追加新菜品
          this.数据源.添加菜品(`新增菜品 ${this.数据源.totalCount()}`);
        })
      }, (菜品: string) => 菜品)
    }
    .cachedCount(5)
  }
}

点击LazyForEach的子组件时,首先调用数据源的添加菜品方法。此方法会在数据源末尾添加数据,并调用notifyDataAdd方法。notifyDataAdd方法内部会调用listener.onDataAdd方法,通知LazyForEach有数据添加。LazyForEach接收到通知后,在该索引处新建子组件。

图4 LazyForEach添加数据

在这里插入图片描述

删除数据操作

class 菜品数据源 extends BasicDataSource {
  private 菜品列表: string[] = [];

  public totalCount(): number {
    return this.菜品列表.length;
  }

  public getData(index: number): string {
    return this.菜品列表[index];
  }

  public 获取所有菜品(): string[] {
    return this.菜品列表;
  }

  public 添加菜品(菜品名: string): void {
    this.菜品列表.push(菜品名);
  }

  public 删除菜品(索引: number): void {
    this.菜品列表.splice(索引, 1);
    this.notifyDataDelete(索引);
  }
}

@Entry
@Component
struct 菜品管理视图 {
  private 数据源: 菜品数据源 = new 菜品数据源();

  aboutToAppear() { 
    for (let i = 0; i <= 20; i++) {
      this.数据源.添加菜品(`经典菜品 ${i}`);
    }
  }

  build() { 
    List({ space: 3 }) {
      LazyForEach(this.数据源, (菜品: string, 索引: number) => {
        ListItem() {
          Row() {
            Text(菜品)
              .fontSize(18)
              .margin(10)
          }
          .padding(15)
          .borderRadius(8)
          .backgroundColor('#FFF8F9FA')
        }
        .onClick(() => {
          // 点击删除当前菜品
          this.数据源.删除菜品(this.数据源.获取所有菜品().indexOf(菜品));
        })
      }, (菜品: string) => 菜品)
    }
    .cachedCount(5)
  }
}

点击LazyForEach的子组件时,调用数据源的删除菜品方法。此方法删除数据源中对应索引的数据,并调用notifyDataDelete方法。notifyDataDelete方法内调用listener.onDataDelete方法,通知LazyForEach删除该索引处的子组件。

图5 LazyForEach删除数据

在这里插入图片描述

交换数据位置

class 菜品数据源 extends BasicDataSource {
  private 菜品列表: string[] = [];

  public totalCount(): number {
    return this.菜品列表.length;
  }

  public getData(index: number): string {
    return this.菜品列表[index];
  }

  public 交换位置(原位置: number, 新位置: number): void {
    const temp = this.菜品列表.splice(原位置, 1);
    this.菜品列表.splice(新位置, 0, temp[0]);
    this.notifyDataMove(原位置, 新位置);
  }

  public 添加菜品(菜品名: string): void {
    this.菜品列表.push(菜品名);
    this.notifyDataAdd(this.菜品列表.length - 1);
  }
}

高级用法实战

改变数据子属性

当需要修改对象数组中某个对象的属性时,需要使用@Observed和@ObjectLink装饰器来监听对象属性的变化:

@Observed 
class 菜品信息 { 
  菜品名称: string;
  价格: number;
  描述: string;
  
  constructor(菜品名称: string, 价格: number, 描述: string) {
    this.菜品名称 = 菜品名称;
    this.价格 = 价格;
    this.描述 = 描述;
  }
} 

@Entry 
@Component 
struct 菜品详情视图 { 
  private 数据源: 菜品数据源 = new 菜品数据源();
  
  aboutToAppear() { 
    for (let i = 0; i <= 20; i++) {
      this.数据源.添加菜品(new 菜品信息(`招牌菜 ${i}`, 28 + i, `这是第${i}道招牌菜的描述`));
    }
  }
  
  build() { 
    List({ space: 3 }) {
      LazyForEach(this.数据源, (菜品: 菜品信息) => {
        ListItem() {
          菜品卡片({ 菜品数据: 菜品 })
        }
      }, (菜品: 菜品信息) => 菜品.菜品名称)
    }
    .cachedCount(5)
  }
} 

@Component 
struct 菜品卡片 { 
  @ObjectLink 菜品数据: 菜品信息;
  
  更新价格() { 
    this.菜品数据.价格 += 5; // 只更新价格属性
  }
  
  build() { 
    Column() {
      Text(this.菜品数据.菜品名称)
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 8 })
        
      Text(`价格: ¥${this.菜品数据.价格}`)
        .fontSize(16)
        .fontColor('#FF666666')
        .margin({ bottom: 5 })
        
      Text(this.菜品数据.描述)
        .fontSize(14)
        .fontColor(Color.Gray)
        .margin({ bottom: 10 })
        
      Button('涨价')
        .onClick(() => this.更新价格())
    }
    .padding(15)
    .borderRadius(8)
    .backgroundColor('#FFF8F9FA')
    .width('100%')
  }
} 

🥦 西兰花小贴士
使用@Observed和@ObjectLink就像给你的菜谱加了"智能标签",什么时候菜的价格变了,标签就会提醒你更新价格牌,不用把整个菜单重新打印~

使用状态管理V2

@ObservedV2与@Trace用于装饰类以及类中的属性,配合使用能深度观测被装饰的类和属性:

@ObservedV2
class 菜品信息 {
  @Trace 菜品名称: string;
  @Trace 价格: number;
  @Trace 描述: string;

  constructor(菜品名称: string, 价格: number, 描述: string) {
    this.菜品名称 = 菜品名称;
    this.价格 = 价格;
    this.描述 = 描述;
  }
}

@Entry
@ComponentV2
struct 高级菜品视图 {
  private 数据源: 菜品数据源 = new 菜品数据Source();

  aboutToAppear() { 
    for (let i = 0; i <= 20; i++) {
      this.数据源.添加菜品(new 菜品信息(`精品菜 ${i}`, 38 + i, `这是第${i}道精品菜的详细描述`));
    }
  }

  build() { 
    List({ space: 3 }) {
      LazyForEach(this.数据源, (菜品: 菜品信息) => {
        ListItem() {
          高级菜品卡片({ 菜品数据: 菜品 })
        }
      }, (菜品: 菜品信息) => 菜品.菜品名称)
    }
    .cachedCount(5)
  }
}

@ComponentV2
struct 高级菜品卡片 {
  @ObjectLink 菜品数据: 菜品信息;

  批量更新() { 
    this.菜品数据.菜品名称 += '【推荐】';
    this.菜品数据.价格 += 10;
    this.菜品数据.描述 += ' 限时优惠!';
  }

  build() { 
    Column() {
      Text(this.菜品数据.菜品名称)
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 10 })
        
      Text(`${this.菜品数据.价格}`)
        .fontSize(18)
        .fontColor('#FFDD0000')
        .margin({ bottom: 8 })
        
      Text(this.菜品数据.描述)
        .fontSize(16)
        .fontColor(Color.Gray)
        .margin({ bottom: 15 })
        
      Button('一键升级')
        .onClick(() => this.批量更新())
    }
    .padding(20)
    .borderRadius(12)
    .backgroundColor(Color.White)
    .width('100%')
    .shadow({
      radius: 4,
      color: '#00000020',
      offsetX: 0,
      offsetY: 2
    })
  }
}

示例中,展示了深度嵌套类结构下,通过@ObservedV2和@Trace实现对多层嵌套属性变化的观测和子组件刷新。当点击子组件Text修改被@Trace修饰的嵌套类属性时,仅重新渲染依赖了该属性的组件。

拖拽排序功能

当LazyForEach在List组件下使用,并且设置了onMove事件,可以使能拖拽排序:

class 菜品数据源 extends BasicDataSource {
  private 菜品列表: string[] = [];

  public totalCount(): number {
    return this.菜品列表.length;
  }

  public getData(index: number): string {
    return this.菜品列表[index];
  }

  public 交换位置不通知(原位置: number, 新位置: number): void {
    const temp = this.菜品列表.splice(原位置, 1);
    this.菜品列表.splice(新位置, 0, temp[0]);
  }

  public 添加菜品(菜品名: string): void {
    this.菜品列表.push(菜品名);
    this.notifyDataAdd(this.菜品列表.length - 1);
  }
}

@Entry
@Component
struct 菜品排序视图 {
  private 数据源: 菜品数据源 = new 菜品数据源();

  aboutToAppear(): void {
    for (let i = 0; i < 100; i++) {
      this.数据源.添加菜品(`招牌菜 ${i}`);
    }
  }

  build() { 
    Row() {
      List() {
        LazyForEach(this.数据源, (菜品: string) => {
          ListItem() {
            Text(菜品)
              .fontSize(16)
              .textAlign(TextAlign.Center)
              .size({ height: 100, width: '100%' })
              .margin(10)
              .borderRadius(10)
              .backgroundColor('#FFFFFFFF')
          }
        }, (菜品: string) => 菜品)
        .onMove((原位置: number, 新位置: number) => {
          this.数据源.交换位置不通知(原位置, 新位置);
        })
      }
      .width('100%')
      .height('100%')
      .backgroundColor('#FFDCDCDC')
    }
  }
}

图12 LazyForEach拖拽排序效果图

在这里插入图片描述

🥦 西兰花小贴士
拖拽排序就像调整菜谱的顺序,你可以把最受欢迎的菜往上挪,把一般的菜往下放,用户体验超棒!

常见问题与解决方案

问题1:渲染结果非预期

问题现象:删除数据时,删除的不是点击的那个项目

// 错误的实现
.onClick(() => {
  // 点击删除子组件
  this.数据源.删除菜品(索引); // 这里的索引可能已经过时
})

图13 LazyForEach删除数据非预期

在这里插入图片描述

原因分析:多次点击子组件时,发现删除的不一定是点击的那个子组件。原因在于删除某个子组件后,该子组件之后的数据项的index应减1,但实际后续数据项对应的子组件仍使用最初分配的index。

解决方案

// 修复后的实现
.onClick(() => {
  // 点击删除子组件
  this.数据源.删除菜品(索引);
  // 重置所有子组件的索引
  this.数据源.重新加载数据();
})

// 键值生成函数也要相应修改
}, (菜品: string, 索引: number) => 菜品 + 索引.toString())

图14 修复LazyForEach删除数据非预期

在这里插入图片描述

问题2:重渲染时图片闪烁

问题现象:只修改了文字内容,但图片出现闪烁

// 问题代码:键值基于message属性
}, (菜品: StringData, 索引: number) => 菜品.message) // 修改message属性会导致键值变化

图15 LazyForEach仅改变文字但是图片闪烁问题

在这里插入图片描述

原因分析:单击ListItem子组件时,只改变了数据项的message属性,但因为键值发生变化,导致整个ListItem被重建。由于Image组件异步刷新,视觉上图片会闪烁。

解决方案:保持键值不变,并使用@ObjectLink和@Observed单独刷新子组件Text

@Observed
class StringData {
  message: string;
  图片资源: Resource;

  constructor(message: string, 图片资源: Resource) {
    this.message = message;
    this.图片资源 = 图片资源;
  }
}

@Component
struct 菜品详情组件 {
  @ObjectLink 菜品数据: StringData;

  build() { 
    Column() {
      Text(this.菜品数据.message).fontSize(50)
      Image(this.菜品数据.图片资源)
        .width(500)
        .height(200)
    }
    .margin({ left: 10, right: 10 })
  }
}

// 键值不受message属性影响
}, (菜品: StringData, 索引: number) => 索引.toString())

🥦 西兰花警告
键值选择是个技术活,千万别用容易变化的数据作为键值!就像做菜时不要用会融化的冰块当标签一样~

性能优化最佳实践

1. 缓存策略

参数 推荐值 说明
cachedCount 3-5 预加载3-5个项目,平衡性能和内存
列表项高度 固定高度 避免高度计算导致的性能问题
图片尺寸 适当压缩 减少内存占用和加载时间

2. 键值选择策略

// ✅ 推荐:使用稳定的唯一标识
(菜品: 菜品信息) => 菜品.唯一ID

// ✅ 推荐:使用索引+数据的组合
(菜品: string, 索引: number) => `${菜品}-${索引}`

// ❌ 不推荐:使用容易变化的数据
(菜品: 菜品信息) => 菜品.显示名称 // 显示名称可能变化

// ❌ 不推荐:使用索引作为唯一键值
(菜品: string, 索引: number) => 索引.toString() // 索引会变化

3. 组件复用优化

结合@Reusable装饰器使用,能触发节点复用:

@Reusable
@Component
struct 可复用菜品卡片 {
  @ObjectLink 菜品数据: 菜品信息;

  build() {
    // 菜品卡片UI
  }
}

// 在LazyForEach中使用
LazyForEach(this.数据源, (菜品: 菜品信息) => {
  ListItem() {
    可复用菜品卡片({ 菜品数据: 菜品 })
  }
}, (菜品: 菜品信息) => 菜品.唯一ID)

4. 内存管理

// ✅ 推荐:及时清理不需要的数据
aboutToDisappear() {
  this.数据源.清理所有数据();
}

// ✅ 推荐:使用弱引用避免循环引用
class 菜品数据源 extends BasicDataSource {
  private 菜品列表: WeakRef<string>[] = []; // 使用弱引用
}

实战案例:电商商品列表

@Observed 
class 商品信息 { 
  商品ID: string;
  商品名称: string;
  价格: number;
  原价: number;
  图片URL: string;
  评分: number;
  销量: number;
  
  constructor(商品ID: string, 商品名称: string, 价格: number, 原价: number, 图片URL: string, 评分: number, 销量: number) {
    this.商品ID = 商品ID;
    this.商品名称 = 商品名称;
    this.价格 = 价格;
    this.原价 = 原价;
    this.图片URL = 图片URL;
    this.评分 = 评分;
    this.销量 = 销量;
  }
} 

class 商品数据源 extends BasicDataSource {
  private 商品列表: 商品信息[] = [];
  private 当前页码: number = 1;
  private 每页数量: number = 20;

  public totalCount(): number {
    return this.商品列表.length;
  }

  public getData(index: number): 商品信息 {
    return this.商品列表[index];
  }

  public 加载更多商品(): void {
    this.当前页码++;
    // 模拟网络请求加载新商品
    this.模拟加载商品数据();
  }

  private 模拟加载商品数据(): void {
    const 起始索引 = this.商品列表.length;
    for (let i = 0; i < this.每页数量; i++) {
      const 商品ID = `商品_${this.当前页码}_${i}`;
      const 商品名称 = `精品商品 ${起始索引 + i}`;
      const 价格 = 99 + (起始索引 + i) % 50;
      const 原价 = 价格 + 20;
      const 图片URL = `https://example.com/product_${起始索引 + i}.jpg`;
      const 评分 = 4.0 + (起始索引 + i) % 10 * 0.1;
      const 销量 = 1000 + (起始索引 + i) * 10;

      this.商品列表.push(new 商品信息(商品ID, 商品名称, 价格, 原价, 图片URL, 评分, 销量));
      this.notifyDataAdd(this.商品列表.length - 1);
    }
  }

  public 初始化数据(): void {
    this.商品列表 = [];
    this.当前页码 = 0;
    this.加载更多商品();
  }
}

@Entry
@Component
struct 商品列表页面 {
  private 数据源: 商品数据源 = new 商品数据源();
  private 是否触底: boolean = false;

  aboutToAppear() {
    this.数据源.初始化数据();
  }

  build() {
    Column() {
      // 页面标题
      Text('精选商品')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin(20)
        .alignSelf(ItemAlign.Center)

      List() {
        LazyForEach(this.数据源, (商品: 商品信息) => {
          ListItem() {
            商品卡片组件({ 商品数据: 商品 })
              .margin({ bottom: 12 })
          }
        }, (商品: 商品信息) => 商品.商品ID)
        .cachedCount(5)
      }
      .onReachEnd(() => {
        this.是否触底 = true;
      })
      .parallelGesture(
        PanGesture({ direction: PanDirection.Up, distance: 50 })
          .onActionStart(() => {
            if (this.是否触底) {
              this.数据源.加载更多商品();
              this.是否触底 = false;
            }
          })
      )
      .padding(16)
      .scrollBar(BarState.Off)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#FFF5F5F5')
  }
}

@Component
struct 商品卡片组件 {
  @ObjectLink 商品数据: 商品信息;

  build() {
    Row() {
      // 商品图片
      Image(this.商品数据.图片URL)
        .width(100)
        .height(100)
        .borderRadius(8)
        .backgroundColor('#FFF0F0F0')
        .margin({ right: 12 })

      Column() {
        // 商品名称
        Text(this.商品数据.商品名称)
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .maxLines(2)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
          .margin({ bottom: 6 })

        // 价格信息
        Row() {
          Text(`${this.商品数据.价格}`)
            .fontSize(18)
            .fontColor('#FFDD0000')
            .fontWeight(FontWeight.Bold)

          Text(`${this.商品数据.原价}`)
            .fontSize(14)
            .fontColor('#FF999999')
            .decoration({ type: TextDecorationType.LineThrough })
            .margin({ left: 8 })
        }
        .margin({ bottom: 4 })

        // 评分和销量
        Row() {
          Text(`${this.商品数据.评分.toFixed(1)}`)
            .fontSize(14)
            .fontColor('#FF666666')

          Text(`已售 ${this.商品数据.销量}`)
            .fontSize(14)
            .fontColor('#FF666666')
            .margin({ left: 16 })
        }
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
    }
    .padding(12)
    .borderRadius(12)
    .backgroundColor(Color.White)
    .width('100%')
    .justifyContent(FlexAlign.SpaceBetween)
    .shadow({
      radius: 2,
      color: '#00000010',
      offsetX: 0,
      offsetY: 1
    })
  }
}

🥦 西兰花小贴士
这个电商商品列表的例子展示了LazyForEach的完整用法:数据懒加载、无限滚动、组件复用、性能优化。实际项目中,你可以根据业务需求调整缓存策略和UI样式~

总结与最佳实践

  1. 数据源设计:实现IDataSource接口,确保数据管理规范化
  2. 键值策略:使用稳定且唯一的标识符,避免基于易变数据
  3. 性能优化:合理设置cachedCount,结合@Reusable使用
  4. 状态管理:复杂对象使用@Observed/@ObjectLink或@ObservedV2/@Trace
  5. 错误处理:处理数据更新异常,提供降级方案
  6. 测试覆盖:测试大量数据场景,确保流畅性

ArkTS的LazyForEach数据懒加载功能是处理海量数据的利器,掌握了它,你就能轻松应对各种大型应用的列表渲染需求。记住:性能 > 炫技,合适的工具用在合适的场景才能发挥最大价值!

推荐资源

📚 官方文档:

📖 进阶学习:


我是盐焗西兰花,
不教理论,只给你能跑的代码和避坑指南。
下期见!🥦

Logo

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

更多推荐