HarmonyOS Repeat 可复用的循环渲染
什么是Repeat
类似懒加载 LazyForEach,都能提高长列表的渲染性能,都能实现组件的动态渲染和释放,节省内存。区别主要是:
Repeat直接监听状态变量的变化,而LazyForEach需要开发者实现IDataSource接口
Repeat使用起来要更加简单一些
Repeat还增强了节点复用能力,提高了长列表滑动和数据更新的渲染性能。
Repeat存在缓冲池,需要复用节点的时候可以从缓冲池中获取,LazyForEach是直接重新创建
Repeat增加了渲染模板(template)的能力,在同一个数组中,根据开发者自定义的模板类型(template type)渲染不同的子组件。
适用于不同结构的组件可以提前把结构定义在模板template中,简化UI结构
Repeat也可以看成是LazyForEach的升级版。
核心概念
关键字 | 说明 |
---|---|
each | 渲染普通节点 |
templateId | 指定要使用的模板的id |
template | 模板 |
key | 要循环渲染的节点的唯一标志 |
virtualScroll | 打开懒加载,totalCount为期望加载的数据长度 |
onMove | 列表项拖拽事件 |
cachedCount | 设置预加载数量和是否开启动画-默认开启 |
组件复用的基本原理
Repeat根据当前的有效加载范围(屏幕可视区域+预加载区域)按需创建子组件
组件复用的过程
首次渲染时
- L1缓存为Repeat有效加载区域
- L2缓存为每个循环渲染模板的空闲节点缓存池。
开始滑动列表
将屏幕向右滑动(屏幕内容右移)一个节点的距离,Repeat将开始复用缓存池中的节点。index=10的节点进入有效加载范围,计算
出其template type为bb。由于bb缓存池非空,Repeat会从bb缓存池中取出一个空闲节点进行复用,更新其节点属性,该子组件中涉
及数据item和索引index的其他孙子组件会根据V2状态管理的规则做同步更新。其他节点仍在有效加载范围,均只更新索引index。
index=0的节点滑出了有效加载范围。当UI主线程空闲时,会检查aa缓存池是否已满,此时aa缓存池未满,将该节点加入到对应的缓
存池中。
如果此时对应template type的缓存池已满,Repeat会销毁掉多余的节点。
数据更新
删除index=4的节点,修改节点数据item_7为new_7。
首先,删除index=4的节点后,失效节点加入aa缓存池。后面的列表节点前移,新进入有效加载区域的节点item_11会复用bb缓存池
中的空闲节点,其他节点均只更新索引index。
最后,节点item_5前移,索引index更新为4。根据template type的计算规则,节点item_5的template type变为aa,需要从aa缓存池中复用空闲节点,并且将旧节点加入bb缓存池
基本使用
对于结构固定的组件,可以使用Repeat和each的简单搭配
// 在List容器组件中使用Repeat
@Entry
@ComponentV2
// 推荐使用V2装饰器
struct RepeatExample {
@Local dataArr: Array<string> = []; // 数据源
aboutToAppear(): void {
for (let i = 0; i < 50; i++) {
this.dataArr.push(`data_${i}`); // 模拟动态加载数据
}
}
build() {
Column() {
List() {
Repeat<string>(this.dataArr)
.each((ri: RepeatItem<string>) => {
ListItem() {
Text('each_' + ri.item).fontSize(30)
}
})
.virtualScroll({ totalCount: this.dataArr.length }) // 打开懒加载,totalCount为期望加载的数据长度
}
.cachedCount(2) // 容器组件的预加载区域大小
.height('70%')
.border({ width: 1 }) // 边框
}
}
}
效果
搭配template使用
templateId指定模板id
template声明模板
// 在List容器组件中使用Repeat
@Entry
@ComponentV2
// 推荐使用V2装饰器
struct RepeatExampleWithTemplates {
@Local dataArr: Array<string> = []; // 数据源
aboutToAppear(): void {
for (let i = 0; i < 50; i++) {
this.dataArr.push(`data_${i}`); // 为数组添加一些数据
}
}
build() {
Column() {
List() {
Repeat<string>(this.dataArr)
.each((ri: RepeatItem<string>) => { // 默认渲染模板
ListItem() {
Text('each_' + ri.item).fontSize(30).fontColor('rgb(161,10,33)') // 文本颜色为红色
}
})
.key((item: string, index: number): string => JSON.stringify(item)) // 键值生成函数
.virtualScroll({ totalCount: this.dataArr.length }) // 打开懒加载,totalCount为期望加载的数据长度
.templateId((item: string, index: number): string => { // 根据返回值寻找对应的模板子组件进行渲染
return index <= 4 ? 'A' : (index <= 10 ? 'B' : ''); // 前5个节点模板为A,接下来的5个为B,其余为默认模板
})
.template('A', (ri: RepeatItem<string>) => { // 'A'模板
ListItem() {
Text('A_' + ri.item).fontSize(30).fontColor('rgb(23,169,141)') // 文本颜色为绿色
}
}, { cachedCount: 3 }) // 'A'模板的缓存列表容量为3
.template('B', (ri: RepeatItem<string>) => { // 'B'模板
ListItem() {
Text('B_' + ri.item).fontSize(30).fontColor('rgb(39,135,217)') // 文本颜色为蓝色
}
}, { cachedCount: 4 }) // 'B'模板的缓存列表容量为4
}
.cachedCount(2) // 容器组件的预加载区域大小
.height('70%')
.border({ width: 1 }) // 边框
}
}
}
效果
更多使用场景
特别注意
节点更新说明
Repeat子组件的节点操作分为四种:节点创建、节点更新、节点复用、节点销毁。其中,节点更新和节点复用的区别为:
- 节点更新:节点不销毁,状态变量驱动节点属性更新。
- 节点复用:旧节点不销毁,存储在空闲节点缓存池;需要创建新节点时,直接从缓存池中获取可复用的旧节点,并做相应的节点属性更新。
使用限制
使用限制
Repeat必须在滚动类容器组件内使用,仅有List、Grid、Swiper以及WaterFlow组件支持Repeat懒加载场景。
循环渲染只允许创建一个子组件,子组件应当是允许包含在容器组件中的子组件。例如:Repeat与List组件配合使用时,子组件必须为ListItem组件。
Repeat不支持V1装饰器,混用V1装饰器会导致渲染异常。
滚动容器组件内只能包含一个Repeat。以List为例,同时包含ListItem、ForEach、LazyForEach的场景是不推荐的;同时包含多个Repeat也是不推荐的。
当Repeat与自定义组件或@Builder函数混用时,必须将RepeatItem类型整体进行传参,组件才能监听到数据变化。详见Repeat与@Builder混用。
关于我们
如果你兴趣想要了解更多的鸿蒙应用开发细节和最新资讯,甚至你想要做出一款属于自己的应用!欢迎在评论区留言或者私信或者看我个人信息,可以加入技术交流群。