Skip to content

列表渲染

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 })  // 只解码 100x100

5.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 必须唯一,列表项固定宽高,图片做下采样"