ArkUI -- 渲染控制:if/else & ForEach & LazyForEach

if/else 条件渲染
渲染、更新机制
if 语句的每个分支都包含一个构造函数,此类构造函数内必须创建一个或多个子组件
- 在初始渲染时,if 语句会执行构造函数,并将生成的子组件添加到其父组件中
- 当 if 或 else if 条件语句中使用的状态变量发生变化时,更新步骤:
- 删除所有以前渲染的组件(以前状态下的分支的构造函数创建的所有子组件)
- 若有 else,或 else if 其它分支,则执行新分支的构造函数,将生成的子组件添加到其父组件中
若条件语句中的子组件被标记为了组件复用(@Reusable),则当其条件变为 false 时,并不会被销毁,而是进入复用池
动态场景下 if 分支切换保护失效
若在动画中 animateTo({}, ()=>{}) 修改判断语句的状态,会对 if 语句下的第一层组件增加默认转场。
若是将判断语句的状态变为 false,即会删除 if 语句下的组件,但由于是在动画中修改的状态,该组件会增加默认转场,会延迟组件的生命周期,即组件没有真正的删除,而是要等到转场动画完成后删除。
@State data: MyData| undefined = new MyData('assassin');
build() {
Column() {
if (this.data) { //在 animateTo 中,修改 this.data = undefined
//由于是在动画中修改的 if 的状态,会对if下的第一层组件,即 Text 增加默认转场,Text 没有真正删除,要等转场动画完成后才删除
//在转场期间,this.data 为 undefined,this.data.name 则会抛异常
Text(this.data.name)
}
Button('动画显示/隐藏').onClick(()=>{
animateTo({}, ()=>{ //在 animateTo 中修改了 if 的判断语句,会给 if 下的第一层组件增加默认转场
if (this.data) {
this.data = undefined;
} else {
this.data = new MyData('assassin');
}
})
})
Button('显示/隐藏').onClick(()=>{
if (this.data) { //直接修改 if 条件,不在动画中,可正常切换
this.data = undefined;
}
})
}
}
对上述情况,有两种修改方案
- 对数据进行判空
if (this.data) {- Text(this.data?.name) } - 对 if/else 下要被删除的组件,添加
transition(TransitionEffect.IDENTITY)属性,避免默认转场if (this.data) {- Text(this.data.name).transition(TransitionEffect.IDENTITY) }
ForEach
ForEach 接口基于数组类型数据来进行循环渲染,需要与容器组件配合使用。
需要注意的是
- ForEach 在创建时,会创建列表中的每个 item 的组件实例, 因此,即使组件被 @Reusable 装饰,列表滑动时,组件也无法复用
- 若 ForEach 的子组件是 ListItem,则其要求 ForEach 的父组件必须为 List 组件
ForEach(
array: Array, //数据源
itemGenerator: (item: itemData, index?: number): void, //组件生成函数
//键值生成函数,返回的 string 用于标识元素的键值,当其键值变化时会基于新的键值创建一个新的组件
keyGenerator?: (item: itemData, index?: number): string => string
)
keyGenerator 键值规则
在 ForEach 循环渲染的过程中,会为每个数组元素生成一个唯一的键值,用于标识对应的组件。当这个键值变化时,会将该数组元素视为已被替换或修改,则会基于新的键值创建一个新的组件。
- ForEach 提供了一个参数
keyGenerator,这是一个函数,返回自定义的键值 - 若没有自定义
keyGenerator函数,ArkUI 会用默认的键值生成函数:keyGenerator: (item: item, index: number) => { return index + '_ ' + JSON.stringify(item); }
键值必须是唯一的,若 ForEach 中有重复的键值,可能无法正常渲染
@State simpleList: Array<string> = ['one', 'two', 'two', 'three'];
build() {
Row() {
Column() {
ForEach(this.simpleList, (item: string) => {
Text(this.item)
}, (item: string) => item) //以 item 作为元素键值
}
.width('100%')
.height('100%')
}
.height('100%')
.backgroundColor(0xF1F3F5)
}
上述 ForEach,以 item 作为元素键值,而数据源 simpleList 中,有两个 ‘two’,即会有两个相同的键值。当 ForEach 遍历到 index = 2 时,发现键值已经存在,则不会再创建新的组件。、
结果预览:
ForEach 子组件创建规则
ForEach 的刷新,需要先感知到数据源发生了变化,接着才会检查键值是否存在,从而判断是刷新组件或创建一个新的组件
ForEach 能感知到数据源变化的情况:
- 用 @State 修饰的数据源 list 的长度发生变化,增加或删除了子元素
- 用 @State 修饰的数据源的 item 的数据非对象
- 首次渲染:为每个数组元素生成唯一的键值,并创建相应的组件
- 非首次渲染:当 ForEach 的数据源发生变化, 需要再次渲染时(@State 感知到数据源的变化),会检查键值是否存在,若不存在则会创建一个新的组件,若键值已存在,则直接渲染该键值所对应的组件
由于 ForEach 的键值默认为:index + '_ ’ + JSON.stringify(item),所以若没有自定义键值,且对数据源数组从头部添加新的数据,则可能所有的元素的键值都不同了(index 改变了),就会重新创建所有的子组件
数据源长度发生变化
若 ForEach 的数据源被 @State 装饰器修饰,对数据源进行增加或删除 item,ArkUI 框架能感知到数据源长度发生变化 ,则会触发 ForEach 进行重新渲染。
数据源 item 对象发生变化
当数据源是一个对象数组时,若只修改数据源的某个 item 的属性值,ArkUI 框架无法监听到 @State 修饰的数据源的属性变化,从而无法触发 ForEach 的重新渲染。
若要实现对属性值变化的 ForEach 重新渲染,需将子组件创建为一个自定义组件,并结合 @Observed 和 @ObjectLink 装饰器使用。
@Observed
class ItemDataBean {
title: string;
content: string;
constructor(title: string, content: string) {
this.title = title;
this.content = content;
}
}
//列表子组件
@Component
struct ItemView {
@State title: string | Resource = '';
@State content: string | Resource = '';
@ObjectLink data: ItemDataBean;
aboutToAppear(): void {
console.info('aboutToAppear ------> ' + this.title)
}
build() {
Column() {
Text(this.title).fontSize(20).fontWeight(600).fontColor('#333')
Text(this.content).fontSize(14).fontColor('#666').margin({top: 12})
Text(this.data.title).fontSize(14).fontColor('#666').margin({top: 12})
Divider().vertical(false).strokeWidth(1)
}.padding(12)
.height(94)
}
}
@Entry
@Component
struct ForEachDemo{
@State
dataList: Array<ItemDataBean> = new Array<ItemDataBean>();
aboutToAppear(): void {
for(let i=0; i<100; i++) {
this.dataList.push(new ItemDataBean(('Title' + i), 'this is content~'));
}
this.dataSrc.setDataList(this.dataList);
}
build() {
Column({space: 10}) {
List() {
ForEach(this.dataList, (item: ItemDataBean, index: number) => {
ItemView({title: item.title, content: item.content, data: item})
.onClick(()=>{
// 方式 1: 创建一个新的对象,替换列表元素
this.dataList[index] = new ItemDataBean(item.title + '~', item.content)
// 方式 2:直接修改 item
item.title = item.title + '~';
})
})
}
}
}
}
上述,对数据源列表元素的修改有两种方式
- 方式一:创建一个新的对象,替换对应索引的元素:
this.dataList[index] = new ItemDataBean( '~~', item.content),数据源能感知到变化,且键值发生了改变 (默认键值),会创建一个新的组件 - 方式1:在 ForEach 内直接修改 item 的值
item.title = item.title + '~';- 子组件 ItemView 的 @ObjectLink 装饰的 data,能感知到数据的变化,子组件刷新 UI(并不会创建新的子组件)
- 子组件 ItemView 的 @State 装饰的 title 无法感知到数据的变化,不会触发 UI 的刷新
此外,若在子组件 ItemView 内添加点击事件,修改 title 或 data ,也会刷新 子组件
LazyForEach 数据懒加载
LazyForEach 从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。当在滚动容器中使用了 LazyForEach,会根据滚动容器可视区域按需创建组件,当组件滑出可视区域外时,框架会进行组件销毁回收以降低内存占用。
- 仅有 List、Grid、Swiper 以及 WaterFlow 支持数据懒加载
- LazyForEach 的父组件,可配置
cachedCount属性,设置可视部分外需加载前后多少数据用于缓冲 - LazyForEach 必须和
@Reusable装饰器一起使用才能触发节点复用 - 懒加载是基于自身和子组件的高度/宽度计算可视范围内的子组件数,若高度/宽度的缺失,会导致懒加载失效,所有子组件都会被创建
LazyForEach(
dataSource: IDataSource, //数据源
itemGenerator: (item: itemData, index: number): void, //组件生成函数
//键值生成函数,返回的 string 用于标识元素的键值,当其键值变化时会基于新的键值创建一个新的组件
keyGenerator?: (item: itemData, index?: number): string => string
)
keyGenerator 键值生成规则
在 LazyForEach 循环渲染过程中,会为每个 item 生成一个唯一且持久的键值,用于标识对应的组件。当这个键值变化时,ArkUI 框架将视为该数组元素已被替换或修改,会基于新的键值创建一个新的组件。
- LazyForEach 提供了一个参数
keyGenerator,这是一个函数,返回自定义的键值 - 若没有自定义
keyGenerator函数,ArkUI 会用默认的键值生成函数:(item: Object, index: number) => { return viewId + '-' + index.toString(); }
viewId 在编译器转换过程中生成,同一个 LazyForEach 组件内其 viewId 是一致的
键值生成器必须针对每个数据生成唯一的值,如果键值相同,将导致键值相同的 UI 组件渲染出现不可预测的问题
LazyForEach 子组件创建规则
- 首次渲染:为每个数组元素生成唯一的键值,并创建相应的组件
- 非首次渲染:当 LazyForEach 数据源发生变化,需要再次渲染时,需要主动调用对应的接口(DataChangeListener ),通知 LazyForEach 更新。
LazyForEach 数据源
LazyForEach 的数据源需实现 IDataSource 接口:
interface IDataSource {
//返回数据源数组长度
totalCount(): number;
//返回指定索引下的元素
getData(index: number): any;
//注册数据变化监听,由框架调用
registerDataChangeListener(listener: DataChangeListener): void;
//去除监听
unregisterDataChangeListener(listener: DataChangeListener): void;
}
数据变化监听器 DataChangeListener,提供了一些列方法通知 LazyForEach 数据更新:
interface DataChangeListener {
// LazyForEach 需要重新加载所有子组件
onDataReloaded(): void;
// 通知 LazyForEach 需要在 index 对应索引处添加子组件
onDataAdd(index: number): void;
// 通知 LazyForEach 将 from 索引处的组件和 to 索引处的组件,进行交换
onDataMove(from: number, to: number): void;
// 通知 LazyForEach 需在 index 对应索引处删除子组件
onDataDelete(index: number): void;
// 通知 LazyForEach 需在 index 对应索引处数据发生了变化,需要重建子组件
onDatasetChange(dataOperations: DataOperation[]): void;
}
创建一个实现了 IDataSource 接口的数据源基类,用于管理 listener 监听,以及通知 LazyForEach 数据更新。
/**
* LazyForEach 数据源基类
* 实现了 IDataSource 接口,用于管理 listener 监听,以及通知 LazyForEach 数据更新
*/
export class BasicDataSource<T> implements IDataSource {
public static TAG: string = 'LazyDataSrc'
private listener: DataChangeListener | null = null;
protected dataList: Array<T> = new Array<T>();
//IDataSource 接口方法
public totalCount(): number {
return this.dataList.length;
}
//IDataSource 接口方法
public getData(index: number): T {
return this.dataList[index];
}
//------------------------ 操作列表数据,并通知 LazyForEach start---------------------------------
public getDataList(): Array<T> {
return this.dataList;
}
public reloadAllData(newDataList: Array<T>): void {
this.dataList = newDataList;
this.notifyDataReload();
}
public pushData(item: T): number {
let size: number = this.dataList.push(item);
this.notifyDataAdd(this.dataList.length -1);
return size;
}
public removeData(index: number): void {
this.dataList.splice(index, 1);
this.notifyDataRemove(index);
}
public changeData(index: number, item: T): void {
this.dataList.splice(index, 1, item);
// this.dataList[index] = item;
this.notifyDataChange(index);
}
public moveData(from: number, to: number): void {
let temp: T = this.dataList[from];
this.dataList[from] = this.dataList[to];
this.dataList[to] = temp;
this.notifyDataMove(from, to);
}
//------------------------ 操作列表数据,并通知 LazyForEach end---------------------------------
/**
* 向 LazyForEach 数据源添加 listener 监听(该方法为框架侧调用)
* @param listener
*/
registerDataChangeListener(listener: DataChangeListener): void {
Logger.i(BasicDataSource.TAG, 'add listener --------->');
this.listener = listener;
}
/**
* 注销监听(该方法为框架侧调用)
* @param listener
*/
unregisterDataChangeListener(listener: DataChangeListener): void {
Logger.i(BasicDataSource.TAG, 'remove listener --------->');
this.listener = null;
}
/**
* 通知 LazyForEach 组件需要重载所有子组件(只有键值改变的 item 才会重建)
*/
private notifyDataReload(): void {
this.listener?.onDataReloaded();
}
/**
* 通知 LazyForEach 需要在 index 对应索引处添加子组件
*/
private notifyDataAdd(index: number): void {
this.listener?.onDataAdd(index);
// 写法2:listener.onDatasetChange([{type: DataOperationType.ADD, index: index}]);
}
/**
* 通知 LazyForEach 在 index 索引处的数据发生了变化,重建对应的子组件
*/
private notifyDataChange(index: number): void {
this.listener?.onDataChange(index);
// 写法2:listener.onDatasetChange([{type: DataOperationType.CHANGE, index: index}]);
}
private notifyDatasetChange(operations: DataOperation[]): void {
this.listener?.onDatasetChange(operations);
}
/**
* 通知 LazyForEach 需在 index 对应索引处删除子组件
*/
private notifyDataRemove(index: number): void {
this.listener?.onDataDelete(index);
//写法2:listener.onDatasetChange([{type: DataOperationType.DELETE, index: index}])
}
/**
* 通知 LazyForEach 将 from 索引处的组件和 to 索引处的组件,进行交换
*/
private notifyDataMove(from: number, to: number): void {
this.listener?.onDataMove(from, to);
//写法2:listener.onDatasetChange([{type: DataOperationType.EXCHANGE, index: {start: from, end: to}}])
}
}
- LazyForEach 必须使用
DataChangeListener对组件进行更新,若直接对数据源 dataSource 重新赋值,会抛异常- 数据源 dataSource 即使使用状态变量,发生变化时也不会触发 LayForEach 的 UI 刷新
- 通过 DataChangeListener 对象的 onDataChange 方法来更新UI时,需要生成不同于原来的键值来触发组件刷新
- onDatasetChange 入参是一个数组,可以一次性通知 LazyForEach 进行数据添加、删除、移动和交换等操作。需要注意的是 onDatasetChange 与其它操作数据的接口不能混用
LazyForEach 使用示例
- LazyForEach 必须在容器组件内使用,仅有 List、Grid、Swiper 以及 WaterFlow 组件支持数据懒加载
- LazyForEach 的父组件,可配置
cachedCount属性,设置可视部分外需加载前后多少数据用于缓冲
创建一个数据源对象:
@Observed
class ItemDataBean {
title: string;
content: string;
constructor(title: string, content: string) {
this.title = title;
this.content = content;
}
}
/**
* LazyForEach 数据源 Demo,继承 BasicDataSource
*/
export class LazyDataSource extends BasicDataSource<ItemDataBean>{
//数据初始化
public setDataList(list: Array<ItemDataBean>) {
this.dataList = list;
}
}
LazyForEach 组件:
@Entry
@Component
struct LazyListDemo {
//dataSrc 不需要 @Stata 装饰
private dataSrc: LazyDataSource = new LazyDataSource();
aboutToAppear(): void {
//创建数据列表
let dataList: Array<ItemDataBean> = new Array<ItemDataBean>();
for(let i=0; i<5; i++) {
dataList.push(new ItemDataBean(('Title' + (i+1)), 'this is content~'));
}
this.dataSrc.setDataList(dataList);
}
build() {
Column({space: 10}) {
//添加一个 item
Button('add item').onClick(()=>{
const newItem = new ItemDataBean(('Title' + (this.dataSrc.totalCount() + 1)), 'this is content~');
this.dataSrc.pushData(newItem);
})
//删除最后一个元素
Button('remove item').onClick(()=>{
this.dataSrc.removeData(this.dataSrc.totalCount()-1);
})
Column() {
List({space: 3}) {
LazyForEach(this.dataSrc, (item: ItemDataBean, index: number) => {
ListItem() {
Column() {
Text(item.title).fontSize(20).fontWeight(600).fontColor('#333')
Text(item.content).fontSize(14).fontColor('#666').margin({top: 12})
Divider().vertical(false).strokeWidth(1)
}.padding(12)
.height(60)
}.onClick(()=>{
// 点击修改 item 的内容,LazyForEach 会重新创建一个新的子组件
item.content = 'this content click ~~~~';
this.dataSrc.changeData(index, item);
})
}, (item: ItemDataBean) => JSON.stringify(item))
}.cachedCount(5)
}.justifyContent(1)
}
}
}
LazyForEach 的数据源发生变化,增加、删除、修改元素,都必须通过 IDataSource 的监听器通知 LazyForEach 刷新 UI
LazyForEach 常见问题
删除结果与预期不符
若上述,ListItem 的点击事件改为删除当前项,并多次点击,会发现删除的并不一定是点击的那个 item。
这是因为,当删除了某个子组件后,位于该组件对应的数据项之后的各项数据的 index 均应减 1,但实际上后续数据项对应的子组件仍然使用的是最初分配的 index,即组件生成函数 itemGenerator 中的 index 并没有发生改变,所以删除结果和预期不符。
为了避免这一问题,需在删除后,再调用接口的 notifyDataReload 方法,通知 LazyForEach 重新加载所有子组件,从而更新 index 索引。
此外,要保证 notifyDataReload 方法能重建组件,需保证数据项能生成新的键值 key,即 keyGenerator 函数需返回自定义的键值,该键值需与 index 相关,如 item + index.toString()。
重新渲染时图片闪烁
若 LazyForEach 的子组件中,有一个图片组件 Image,当需要重建该组件时:
- 通过
notifyDataChange(index)通知 LazyForEach 刷新该组件,会重建组件 notifyDataReload通知重新加载所有组件,对键值变化的组件,会被重建
由于 Image 组件是异步刷新,所以视觉上图片会发生闪烁。为了解决这种情况,应该使用 @ObjectLink 和 @Observed 去单独刷新组件。
在 List 内使用屏幕闪烁
在 List 的 onScrollIndex 方法中调用 onDataReloaded 有产生屏幕闪烁的风险。
应用 onDatasetChange 代替 onDataReloaded,不仅可以修复闪屏的问题,还能提升加载性能。
组件复用渲染异常
@Reusable 与 @ComponentV2 混用会导致组件渲染异常。
在 @ComponentV2 装饰的组件中,LazyForEach 的自定义子组件使用了 @Reusable 装饰,会导致组件渲染失败,该组件的生命周期函数只触发了 onAppear,但是没有触发 aboutToAppear。
若要用 @Reusable 装饰子组件,LazyForEach 的父组件必须用 @Component 而不能是 @ComponentV2。
可复用子组件的生命周期函数:
- 首次创建,触发
aboutToAppear - 滑动 LazyForEach,子组件不可见,该子组件会被加入到复用缓冲中,并不会被销毁,并触发
aboutToRecycle - 当滑动到子组件可见,会将可复用的组件从复用缓存中重新加入到节点树,并触发
aboutToReuse刷新组件
数据源 item 属性变化
若仅靠 LazyForEach 的刷新机制,当 item 的数据发生变化时,需将原来的子组件全部销毁再重建,在子组件结构较为复杂的情况下,靠改变键值去刷新渲染性能较低。
框架提供了 @Observed 和 @ObjectLink 机制,将 LazyForEach 的子组件作为独立的自定义组件,通过 @ObjectLink 成员变量对其子属性的监听,只会刷新对应的子组件,不会去重建整个子组件,提高了渲染性能。
@ObservedV2 修饰的 对象中,被 @Trace 装饰器修饰的属性能被深度观察
@ObservedV2 和 @Trace
ObjectLink 装饰的成员变量仅能监听到其子属性的变化,再深入嵌套的属性便无法观测到了
状态管理 V2 提供了 @ObservedV2 和 @Trace 装饰器,可以实现对属性的深度观测。
@ObservedV2 和 @Trace 用于装饰类和类中的属性,配合使用能深度观测被修饰的类和属性。
class ItemData {
title: string;
content: ContentData;
constructor(title: string, content: ContentData) {
this.title = title;
this.content = content;
}
}
@ObservedV2
class ContentData {
subTitle: string = '';
//@ObservedV2 对象中,被 @Trace 装饰器,修饰的属性能被深度观察
@Trace likeCount: number = 0;
}
在深度嵌套类结构下,通过 @ObservedV2 和 @Trace 实现对多层嵌套属性变化的观测。当 @Trace 修饰的属性 (上述的 likeCount 属性) 发生变化,仅会重新渲染依赖该属性的组件。
更多推荐

所有评论(0)