及时做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懒加载的实现和优化:
- 数据源实现:实现IDataSource和DataChangeListener接口
- 页面集成:在List组件中使用LazyForEach
- 数据更新:通过数据源方法触发UI更新
- 性能提升:大幅减少首屏渲染时间和内存占用
LazyForEach是处理长列表的最佳选择,合理使用可以显著提升应用性能。