前面文章我们介绍过@Reusable装饰器
实现组件实例的复用机制以提升性能, 其中示例用到了LazyForEach,也就是本章的主角。LazyForEach是鸿蒙ArkUI框架中实现高性能列表渲染的核心组件,其核心设计思想基于动态加载和资源回收机制。
- LazyForEach基于
动态加载+视图复用
的双重优化机制 - LazyForEach必须在容器组件内使用,仅有List、Grid、Swiper以及WaterFlow组件支持数据懒加载
- LazyForEach必须和@Reusable装饰器一起使用才能触发节点复用
- LazyForEach必须使用DataChangeListener对象来进行更新
- 可视区域优先:仅渲染当前屏幕可视区域内的列表项(如List的可见部分)
- 缓冲区策略:通过
cachedCount
配置载前后缓冲区(默认0),预加载不可见区域的部分元素,降低滑动白屏概率,推荐值为可视区域能显示项数的1-2倍 - 节点回收:使用
@Reusable
装饰器标记可复用组件,滑出可视区的组件存入复用池优先复用 - 组件结构:每个迭代必须返回单一根组件,支持嵌套条件渲染
- 唯一性原则:同一容器内只能存在一个LazyForEach,且不支持嵌套其他迭代组件
与ForEach的关键差异
特性 | LazyForEach | ForEach |
---|---|---|
渲染策略 | 按需加载+节点回收 | 全量渲染 |
内存占用 | 动态控制(更低) | 固定(更高) |
适用场景 | 长列表/复杂项 | 短列表/简单项 |
性能优化 | 自动回收+复用 | 无特殊优化 |
数据更新 | 需DataChangeListener | 直接响应式更新 |
基础使用示例
scss
@Entry
@Component
struct DemoList {
private data: MyDataSource = new MyDataSource();
build() {
List({ space: 10 }) {
LazyForEach(this.data, (item) => {
@Reusable
@Component
struct ListItem {
@Prop item: string
build() {
Row() {
Image(item.avatar).width(50).height(50)
Column() {
Text(item.name).fontSize(16)
Text(item.description).opacity(0.6)
}
}
}
}
ListItem({ item })
}, (item) => item.id.toString())
.cachedCount(3) // 缓冲区配置
}
}
}
数据源实现
- 必须实现
IDataSource
接口:
kotlin
class BasicDataSource implements IDataSource {
private listeners: DataChangeListener[] = [];
private originDataArray: string[] = [];
public totalCount(): number {
return 0;
}
public getData(index: number): string {
return this.originDataArray[index];
}
// 该方法为框架侧调用,为LazyForEach组件向其数据源处添加listener监听
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
console.info('add listener');
this.listeners.push(listener);
}
}
// 该方法为框架侧调用,为对应的LazyForEach组件在数据源处去除listener监听
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
console.info('remove listener');
this.listeners.splice(pos, 1);
}
}
// 通知LazyForEach组件需要重载所有子组件
notifyDataReload(): void {
this.listeners.forEach(listener => {
listener.onDataReloaded();
})
}
// 通知LazyForEach组件需要在index对应索引处添加子组件
notifyDataAdd(index: number): void {
this.listeners.forEach(listener => {
listener.onDataAdd(index);
})
}
// 通知LazyForEach组件在index对应索引处数据有变化,需要重建该子组件
notifyDataChange(index: number): void {
this.listeners.forEach(listener => {
listener.onDataChange(index);
})
}
// 通知LazyForEach组件需要在index对应索引处删除该子组件
notifyDataDelete(index: number): void {
this.listeners.forEach(listener => {
listener.onDataDelete(index);
})
}
// 通知LazyForEach组件将from索引和to索引处的子组件进行交换
notifyDataMove(from: number, to: number): void {
this.listeners.forEach(listener => {
listener.onDataMove(from, to);
})
}
}
class MyDataSource extends BasicDataSource {
private dataArray: string[] = [];
public totalCount(): number {
return this.dataArray.length;
}
public getData(index: number): string {
return this.dataArray[index];
}
public addData(index: number, data: string): void {
this.dataArray.splice(index, 0, data);
this.notifyDataAdd(index);
}
public pushData(data: string): void {
this.dataArray.push(data);
this.notifyDataAdd(this.dataArray.length - 1);
}
}
数据更新
graph TD
A[数据变更] --> B{操作类型}
B -->|添加| C[notifyDataAdd]
B -->|删除| D[notifyDataDelete]
B -->|修改| E[notifyDataChange]
B -->|移动| F[notifyDataMove]
严禁直接修改数据源数组,必须通过监听器通知变更
@Reusable装饰器深度集成:复用机制实现
typescript
@Reusable
@Component
struct NewsListItem {
@Prop article: Article;
aboutToReuse(params: { newArticle: Article }) {
this.article = params.newArticle; // 复用时的状态更新
}
build() {
Column() {
NetworkImage(this.article.cover)
Text(this.article.title)
}
}
}
注意事项:
- 复用组件必须使用
@Prop
而非@Link
接收数据- 避免在复用组件中使用
@StorageLink
等持久化状态装饰器
复用性能优化策略
优化方向 | 实施方法 | 效果 |
---|---|---|
内存优化 | 设置.maxReuseCount(10) 限制复用池大小 |
减少30%内存占用 |
渲染优化 | 在aboutToReuse 中重置动画状态 |
避免复用导致的动画残留 |
网络优化 | 使用Image.syncLoad(true) 预加载图片 |
降低图片闪烁概率 |
常见问题
-
问题1:列表项错乱
- 根因:键值生成规则不唯一
- 解决方案:避免使用index作为唯一键,采用复合键
${id}_${timestamp}
-
问题2:图片闪烁
- 根因:复用导致Image重新加载
- 优化方案:
typescript@Reusable @Component struct StableImage { @Prop url: string; private cachedImage = new LRUCache(20); build() { Image(this.cachedImage.get(this.url) || fetchImage(this.url)) } }
-
索引错位问题
- 场景:删除中间项后点击事件错位
- 解决方案:
kotlintypescript // 使用唯一标识代替index onClick(() => { const targetId = this.data.getId(index); this.data.deleteById(targetId); })
-
异构列表实现
typescript
LazyForEach(dataSource, (item) => {
if (item.type === 'BANNER') {
BannerItem(item)
} else if (item.type === 'VIDEO') {
VideoItem(item)
} else {
DefaultItem(item)
}
}, item => `${item.type}_${item.id}`)
总结
LazyForEach作为鸿蒙长列表渲染的核心方案,其高效性来源于精妙的动态加载+组件复用双重机制。开发者需特别注意:
- 键值生成规则必须保证全局唯一性
- 数据更新必须通过
DataChangeListener
规范操作 - 结合
@Reusable
实现组件级复用优化 - 合理配置
cachedCount
平衡性能与内存