Appearance
列表渲染
ForEach vs LazyForEach 是 ArkUI 列表渲染的核心,理解它们的工作原理和使用场景是关键。
1. ForEach — 全量渲染
1.1 基本用法
typescript
@Component
struct Index {
@State items: string[] = ['Item 1', 'Item 2', 'Item 3']
build() {
Column() {
ForEach(this.items, (item: string, index: number) => {
Text(item)
.fontSize(16fp)
.padding(12)
.backgroundColor(Color.FromRGB(0xF5, 0xF5, 0xF5))
.margin({ bottom: 8 })
.borderRadius(4)
}, (item: string) => item) // keyGenerator
}
.width('100%')
}
}1.2 keyGenerator 的重要性
typescript
// ❌ 错误:不指定 keyGenerator
ForEach(this.items, (item) => { ... })
// ✅ 正确:指定 keyGenerator(性能关键!)
ForEach(this.items, (item) => { ... }, (item) => item.id)
// ✅ 正确:组合 key
ForEach(this.items, (item) => { ... }, (item) => `${item.type}_${item.id}`)1.3 ForEach 的限制
| 限制 | 说明 |
|---|---|
| 全量渲染 | 所有数据一次性创建组件节点 |
| 数据量大时 | 内存占用大、卡顿 |
| 适用场景 | 少量数据(< 100 条) |
2. LazyForEach — 按需渲染 ⭐
2.1 基本用法
typescript
@Entry
@Component
struct Index {
@State items: Array<{ id: string, name: string }> = []
private myDataSource: MyDataSource = new MyDataSource()
aboutToAppear() {
// 初始化数据
this.myDataSource.refreshData()
}
build() {
Column() {
LazyForEach(this.myDataSource, (item: any) => {
ListItem() {
Row() {
Text(item.name)
.fontSize(16fp)
.fontWeight(FontWeight.Bold)
Blank()
Text(`¥${item.price}`)
.fontSize(16fp)
.fontColor(Color.Red)
}
.width('100%')
.padding(16)
}
}, (item: any) => item.id) // keyGenerator
Divider()
}
.width('100%')
.height('100%')
}
}
// IDataSource 实现
class MyDataSource implements IDataSource {
private mData: Array<{ id: string, name: string, price: number }> = []
private listener: DataChangeListener | undefined
refreshData(): void {
// 加载数据(可以是网络请求、数据库查询等)
this.mData = Array.from({ length: 1000 }, (_, i) => ({
id: `item_${i}`,
name: `商品 ${i + 1}`,
price: (Math.random() * 1000).toFixed(2)
}))
// 通知数据已加载
if (this.listener) {
this.listener.onDataReload(this)
}
}
// 获取 index 位置的数据
getData(index: number): any {
return this.mData[index]
}
// 获取数据总数
getCount(): number {
return this.mData.length
}
// 注册数据变化监听器
registerDataChangeListener(listener: DataChangeListener): void {
this.listener = listener
}
// 取消注册
unregisterDataChangeListener(listener: DataChangeListener): void {
this.listener = undefined
}
}2.2 LazyForEach 的工作原理
LazyForEach 渲染流程:
1. 计算可视区域(屏幕范围内的数据范围)
2. 只创建可视区域内的组件节点
3. 滑动时:
├─ 离开可视区域的节点 → 放入节点池缓存
├─ 新进入可视区域 → 复用节点池中的缓存节点
└─ 更新数据(避免重新创建)
效果:10000 条数据只创建 ~10 个节点2.3 LazyForEach 的使用限制
| 限制 | 说明 |
|---|---|
| 外层容器 | 必须是 List/Grid/Swiper |
| 子组件 | 必须是统一类型 |
| keyGenerator | 必须提供,且 key 必须唯一 |
| 数据源 | 必须实现 IDataSource 接口 |
3. LazyForEach + List 组合(最常用)
typescript
@Entry
@Component
struct Index {
@State loading: boolean = false
private myDataSource: MyDataSource = new MyDataSource()
private listScroller: ListScroller = new ListScroller()
aboutToAppear() {
this.myDataSource.refreshData()
}
build() {
Column() {
// 顶部搜索栏
Row() {
TextInput({ placeholder: '搜索商品' })
.layoutWeight(1)
Button('搜索')
.margin({ left: 8 })
}
.padding(12)
.backgroundColor(Color.White)
// 列表
List({ scroller: this.listScroller }) {
LazyForEach(this.myDataSource, (item: any) => {
ListItem() {
ListItemCard(item)
}
}, (item: any) => item.id)
}
.layoutWeight(1)
.width('100%')
.edgePadding({ top: 8, bottom: 8 })
.scrollBar(BarState.Auto)
.divider({
strokeColor: Color.FromRGB(0xEE, 0xEE, 0xEE),
strokeWidth: 1,
startMargin: 16,
endMargin: 16
})
}
.width('100%')
.height('100%')
.backgroundColor(Color.FromRGB(0xF8, 0xF8, 0xF8))
}
}
// 列表项卡片组件
@Component
struct ListItemCard {
item: any
build() {
Row() {
Image($r('app.media.product_icon'))
.width(80)
.height(80)
.borderRadius(4)
Column() {
Text(this.item.name)
.fontSize(16fp)
.fontWeight(FontWeight.Bold)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text('品牌旗舰店')
.fontSize(12fp)
.fontColor(Color.Gray)
.margin({ top: 4 })
Text(`¥${this.item.price}`)
.fontSize(18fp)
.fontColor(Color.Red)
.margin({ top: 4 })
}
.margin({ left: 12 })
.layoutWeight(1)
}
.width('100%')
.padding(12)
.backgroundColor(Color.White)
.borderRadius(8)
}
}4. LazyForEach + Grid
typescript
@Entry
@Component
struct Index {
private imageDataSource: ImageDataSource = new ImageDataSource()
build() {
Column() {
Grid() {
LazyForEach(this.imageDataSource, (item: ImageItem) => {
GridItem() {
Image(item.url)
.width('100%')
.aspectRatio(1)
.objectFit(ImageFit.Cover)
}
}, (item: ImageItem) => item.id)
}
.columnsTemplate('repeat(3, 1fr)')
.rowsTemplate('auto')
.gap(4)
.width('100%')
}
.width('100%')
.height('100%')
}
}5. 列表性能优化技巧
5.1 固定列表项宽高
typescript
// ❌ 错误:不固定宽高,每行都需要测量
LazyForEach(dataSource, (item) => {
ListItem() {
Column() {
Text(item.name)
Text(item.desc)
}
}
})
// ✅ 正确:固定宽高(避免反复测量)
LazyForEach(dataSource, (item) => {
ListItem() {
Column() {
Text(item.name).height(40)
Text(item.desc).height(30)
}
.height(70) // 固定容器高度
}
.height(70) // 固定 ListItem 高度
})5.2 图片下采样
typescript
// ❌ 错误:大图不缩放
Image(largeImage)
.width(100)
.height(100)
// ✅ 正确:sourceSize 指定解码尺寸
Image(largeImage)
.width(100)
.height(100)
.sourceSize({ width: 100, height: 100 }) // 只解码 100x1005.3 减少 build 复杂度
typescript
// ❌ 错误:build 中嵌套太深
LazyForEach(dataSource, (item) => {
ListItem() {
Column() { // 第1层
Row() { // 第2层
Stack() { // 第3层
Column() { // 第4层
Text('内容')
}
}
}
}
}
})
// ✅ 正确:提取为子组件
@Component
struct ItemCard { ... }
LazyForEach(dataSource, (item) => {
ListItem() {
ItemCard({ item })
}
})5.4 keyGenerator 必须唯一
typescript
// ❌ 错误:key 不唯一会导致渲染异常
LazyForEach(dataSource, (item) => { ... }, (item) => item.category)
// ✅ 正确:组合 key 保证唯一
LazyForEach(dataSource, (item) => { ... }, (item) => `${item.category}_${item.id}`)6. 列表滚动控制
6.1 ListScroller
typescript
private listScroller: ListScroller = new ListScroller()
// 滚动到指定位置
this.listScroller.scrollToIndex(100)
this.listScroller.scrollToOffset(500)
// 获取滚动位置
let offset = this.listScroller.currentOffset()
// 监听滚动位置
this.listScroller.on('scroll', (info: ScrollState) => {
console.log('当前偏移:', info.offset)
console.log('可见范围:', info.visibleRange)
})
// 监听滚动帧
this.listScroller.on('scrollFrame', (offset: number) => {
// 每帧回调(用于下拉刷新、加载更多等)
})7. 面试高频考点
Q1: ForEach 和 LazyForEach 的区别?
回答:ForEach 全量渲染,适合少量数据;LazyForEach 按需渲染(只创建可视区域内组件),配合 IDataSource 使用,长列表必用。
Q2: LazyForEach 的使用限制?
回答:外层必须是 List/Grid/Swiper,子组件必须是统一类型,keyGenerator 必须唯一。
Q3: 列表滑动卡顿怎么排查?
回答:1. 检查是否用了 LazyForEach;2. 检查列表项是否设置了固定宽高;3. 检查 build 中是否有耗时逻辑;4. 检查图片是否过大(需下采样)。
🐱 小猫提示:列表渲染记住 "长列表必用 LazyForEach + IDataSource,key 必须唯一,列表项固定宽高,图片做下采样"。