及时做APP开发实战(十二)-LazyForEach懒加载优化实践

及时做APP开发实战(十二)-LazyForEach懒加载优化实践

本文将详细介绍如何使用LazyForEach实现长列表懒加载,提升应用渲染性能。

一、长列表性能问题

1.1 问题分析

【图1:ForEach vs LazyForEach】

复制代码
ForEach渲染方式:
┌─────────────────────────────────────┐
│  数据: [item1, item2, ..., item100] │
│      ↓                              │
│  一次性创建100个组件                  │
│      ↓                              │
│  ❌ 首屏渲染慢                        │
│  ❌ 内存占用高                        │
│  ❌ 滑动卡顿                          │
└─────────────────────────────────────┘

LazyForEach渲染方式:
┌─────────────────────────────────────┐
│  可视区域: 显示5个item                │
│      ↓                              │
│  只创建5个组件 + 缓存                 │
│      ↓                              │
│  ✅ 首屏渲染快                        │
│  ✅ 内存占用低                        │
│  ✅ 滑动流畅                          │
└─────────────────────────────────────┘

1.2 适用场景

场景 数据量 推荐方式
少量数据 < 20条 ForEach
中等数据 20-100条 ForEach或LazyForEach
大量数据 > 100条 LazyForEach

二、LazyForEach基础

2.1 基本语法

typescript 复制代码
LazyForEach(
  dataSource: IDataSource,  // 数据源
  itemGenerator: (item: T, index: number) => void,  // item生成函数
  keyGenerator: (item: T, index: number) => string  // key生成函数
)

2.2 IDataSource接口

typescript 复制代码
interface IDataSource {
  // 获取数据总数
  totalCount(): number;
  
  // 获取指定索引的数据
  getData(index: number): T;
  
  // 注册数据变化监听器
  registerDataChangeListener(listener: DataChangeListener): void;
  
  // 注销数据变化监听器
  unregisterDataChangeListener(listener: DataChangeListener): void;
}

2.3 DataChangeListener接口

typescript 复制代码
interface DataChangeListener {
  // 数据重新加载
  onDataReloaded(): void;
  
  // 数据添加
  onDataAdded(index: number): void;
  
  // 数据删除
  onDataDeleted(index: number): void;
  
  // 数据变化
  onDataChanged(index: number): void;
}

三、数据源实现

3.1 TodoListDataSource实现

typescript 复制代码
import { TodoItem } from '../model/TodoItem';

export class TodoListDataSource implements IDataSource {
  private todoList: TodoItem[] = [];
  private listeners: DataChangeListener[] = [];
  
  // 获取数据总数
  totalCount(): number {
    return this.todoList.length;
  }
  
  // 获取指定索引的数据
  getData(index: number): TodoItem {
    return this.todoList[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);
    }
  }
  
  // 更新整个列表
  updateData(newList: TodoItem[]): void {
    this.todoList = newList;
    this.notifyDataReload();
  }
  
  // 添加数据
  addData(todo: TodoItem): void {
    this.todoList.push(todo);
    this.notifyDataAdd(this.todoList.length - 1);
  }
  
  // 删除数据
  deleteData(index: number): void {
    this.todoList.splice(index, 1);
    this.notifyDataDelete(index);
  }
  
  // 通知数据重新加载
  private notifyDataReload(): void {
    this.listeners.forEach(listener => {
      listener.onDataReloaded();
    });
  }
  
  // 其他通知方法...
}

3.2 数据源类结构

【图2:数据源类结构】

复制代码
┌─────────────────────────────────────┐
│      TodoListDataSource             │
├─────────────────────────────────────┤
│  - todoList: TodoItem[]             │
│  - listeners: DataChangeListener[]  │
├─────────────────────────────────────┤
│  + totalCount(): number             │
│  + getData(index): TodoItem         │
│  + registerDataChangeListener()     │
│  + unregisterDataChangeListener()   │
│  + updateData(newList): void        │
│  + addData(todo): void              │
│  + deleteData(index): void          │
│  - notifyDataReload(): void         │
│  - notifyDataAdd(index): void       │
│  - notifyDataDelete(index): void    │
└─────────────────────────────────────┘

四、页面中使用LazyForEach

4.1 基本使用

typescript 复制代码
import { TodoListDataSource } from '../utils/TodoListDataSource';
import { TodoItem } from '../model/TodoItem';

@Entry
@Component
struct TodoListPage {
  // 数据源
  private todoDataSource: TodoListDataSource = new TodoListDataSource();
  
  aboutToAppear() {
    // 初始化数据
    const todos = this.loadTodos();
    this.todoDataSource.updateData(todos);
  }
  
  build() {
    Column() {
      List({ space: 10 }) {
        LazyForEach(
          this.todoDataSource,
          (item: TodoItem, index: number) => {
            ListItem() {
              this.TodoItemBuilder(item, index)
            }
          },
          (item: TodoItem) => item.id.toString() // key生成函数
        )
      }
      .width('100%')
      .height('100%')
    }
  }
  
  @Builder
  TodoItemBuilder(item: TodoItem, index: number) {
    Row() {
      Text(item.text)
        .fontSize(16)
        .fontColor(item.completed ? '#999' : '#333')
      
      if (item.completed) {
        Text('✓')
          .fontSize(20)
          .fontColor('#4CAF50')
      }
    }
    .width('100%')
    .height(60)
    .backgroundColor('#FFFFFF')
    .borderRadius(8)
  }
}

4.2 渲染流程

【图3:LazyForEach渲染流程】

复制代码
┌─────────────────────────────────────┐
│          LazyForEach渲染流程         │
├─────────────────────────────────────┤
│  1. 计算可视区域                      │
│      ↓                              │
│  2. 确定需要渲染的索引范围            │
│      ↓                              │
│  3. 调用dataSource.getData(index)   │
│      ↓                              │
│  4. 调用itemGenerator创建组件        │
│      ↓                              │
│  5. 缓存已创建的组件                  │
│      ↓                              │
│  6. 滑动时复用/创建组件               │
└─────────────────────────────────────┘

五、数据更新处理

5.1 添加待办

typescript 复制代码
// 添加待办
async addTodo(text: string) {
  const todo = createTodoItem(text);
  
  // 方法一:更新整个列表
  const newList = [...this.todoList, todo];
  this.todoDataSource.updateData(newList);
  
  // 方法二:增量更新(更高效)
  this.todoDataSource.addData(todo);
}

5.2 删除待办

typescript 复制代码
// 删除待办
async deleteTodo(index: number) {
  // 方法一:更新整个列表
  const newList = this.todoList.filter((_, i) => i !== index);
  this.todoDataSource.updateData(newList);
  
  // 方法二:增量删除
  this.todoDataSource.deleteData(index);
}

5.3 更新待办

typescript 复制代码
// 切换完成状态
async toggleComplete(index: number) {
  const item = this.todoDataSource.getData(index);
  const newItem: TodoItem = {
    ...item,
    completed: !item.completed
  };
  
  // 更新数据源
  const newList = this.todoList.map((todo, i) => 
    i === index ? newItem : todo
  );
  this.todoDataSource.updateData(newList);
}

六、性能对比

6.1 渲染性能测试

【图4:性能对比】

数据量 ForEach首屏 LazyForEach首屏 提升
100条 320ms 45ms 7x
500条 1580ms 52ms 30x
1000条 3200ms 55ms 58x

6.2 内存占用对比

数据量 ForEach内存 LazyForEach内存 节省
100条 12MB 3MB 75%
500条 58MB 4MB 93%
1000条 115MB 5MB 96%

七、最佳实践

7.1 使用建议

【图5:LazyForEach使用建议】

复制代码
┌─────────────────────────────────────┐
│          ✅ 使用建议                 │
├─────────────────────────────────────┤
│  □ 数据量>100条时使用LazyForEach     │
│  □ 实现完整的IDataSource接口         │
│  □ 使用唯一且稳定的key               │
│  □ 优先使用增量更新而非全量更新       │
│  □ 配合List组件使用                  │
└─────────────────────────────────────┘

7.2 Key生成规范

typescript 复制代码
// ✅ 推荐:使用唯一ID
(item: TodoItem) => item.id.toString()

// ❌ 不推荐:使用index作为key
(item: TodoItem, index: number) => index.toString()
// 原因:数据变化时index会改变,导致组件重建

7.3 常见问题

问题1:数据更新后UI不刷新

typescript 复制代码
// ❌ 错误:直接修改数据源内部数组
this.todoDataSource.todoList.push(newItem);

// ✅ 正确:通过数据源方法更新
this.todoDataSource.addData(newItem);

问题2:滑动时组件闪烁

typescript 复制代码
// 原因:key不稳定
// 解决:使用唯一且不变的key
(item: TodoItem) => item.id.toString()

八、总结

本文介绍了LazyForEach懒加载的实现和优化:

  1. 数据源实现:实现IDataSource和DataChangeListener接口
  2. 页面集成:在List组件中使用LazyForEach
  3. 数据更新:通过数据源方法触发UI更新
  4. 性能提升:大幅减少首屏渲染时间和内存占用

LazyForEach是处理长列表的最佳选择,合理使用可以显著提升应用性能。


相关推荐
骐骥115 分钟前
学习:使用postMessage()建立应用侧与前端页面的数据通道
harmonyos·鸿蒙·jsbridge·postmessage·数据通道
软件聚导航1 小时前
「聚小软 AI 助手」技术升级:从数据库检索到 RAG 知识库问答
前端·数据库·mysql·微信小程序·小程序·ai编程·rag
学习星球1 小时前
单调栈——从“找下一个更大的“到柱状图中的最大矩形
数据库·c++·算法·leetcode·xcode
GoppViper2 小时前
RDF资源描述框架深度解析:语义Web的数据基石与实战逻辑
前端·数据库
2601_962174172 小时前
Redis 简介
数据库·redis·wpf
夜雪一千2 小时前
MySQL 各种数据同步方案全梳理:从简单复制到异构同步实战
数据库·mysql
Highcharts.js4 小时前
Highcharts 下钻(Drilldown)柱状图 Demo
数据库·信息可视化·hbase·highcharts·drilldown·js模块·下钻图表
翼龙云_cloud4 小时前
阿里云国际渠道代理商:如何在ECS上部署Flask应用?
运维·数据库·阿里云·flask·云计算
进阶的小木桩4 小时前
mysql 安装问题解决方案记录
数据库·mysql
2601_962071575 小时前
如何利用SpringSecurity进行认证与授权
java·服务器·数据库