ArkTS三种渲染控制机制
ArkUI通过自定义组件的build()函数和@builder装饰器中的声明式UI描述语句构建相应的UI。在声明式描述语句中开发者除了使用系统组件外,还可以使用渲染控制语句来辅助UI的构建,这些渲染控制语句包括控制组件是否显示的条件渲染语句,基于数组数据快速生成组件的循环渲染语句以及针对大数据量场景的数据懒加载语句。
1. if/else:条件渲染
ArkTS提供了渲染控制的能力。条件渲染可根据应用的不同状态,使用if、else和else if渲染对应状态下的UI内容。
1.1. 使用规则
-
支持if、else和else if语句。
-
if、else if后跟随的条件语句可以使用状态变量。
-
允许在容器组件内使用,通过条件渲染语句构建不同的子组件。
-
条件渲染语句在涉及到组件的父子关系时是“透明”的,当父组件和子组件之间存在一个或多个if语句时,必须遵守父组件关于子组件使用的规则。
-
每个分支内部的构建函数必须遵循构建函数的规则,并创建一个或多个组件。无法创建组件的空构建函数会产生语法错误。
-
某些容器组件限制子组件的类型或数量,将条件渲染语句用于这些组件内时,这些限制将同样应用于条件渲染语句内创建的组件。例如,Grid容器组件的子组件仅支持GridItem组件,在Grid内使用条件渲染语句时,条件渲染语句内仅允许使用GridItem组件。
1.2. 更新机制
当if、else if后跟随的状态判断中使用的状态变量值变化时,条件渲染语句会进行更新,更新步骤如下:
-
评估if和else if的状态判断条件,如果分支没有变化,无需执行以下步骤。如果分支有变化,则执行2、3步骤:
-
删除此前构建的所有子组件。
-
执行新分支的构造函数,将获取到的组件添加到if父容器中。如果缺少适用的else分支,则不构建任何内容。
条件可以包括Typescript表达式。对于构造函数中的表达式,此类表达式不得更改应用程序状态。
1.3. 常用的if ... else
以下示例包含if ... else ...语句与拥有@State装饰变量的子组件。
@Component
struct CounterView {@State counter: number = 0;label: string = 'unknown';
build() {Row() {Text(`${this.label}`)Button(`counter ${this.counter} +1`).onClick(() => {this.counter += 1;})}}
}
@Entry
@Component
struct MainView {@State toggle: boolean = true;
build() {Column() {if (this.toggle) {CounterView({ label: 'CounterView #positive' })} else {CounterView({ label: 'CounterView #negative' })}Button(`toggle ${this.toggle}`).onClick(() => {this.toggle = !this.toggle;})}}
}
2. ForEach:循环渲染
太简单常用了,就不举例了。
用法
ForEach(arr: Array,itemGenerator: (item: any, index: number) => void,keyGenerator?: (item: any, index: number) => string
)
2.1. 键值生成规则
在ForEach循环渲染过程中,系统会为每个数组元素生成一个唯一且持久的键值,用于标识对应的组件。当这个键值变化时,ArkUI框架将视为该数组元素已被替换或修改,并会基于新的键值创建一个新的组件。
2.2. 组件创建规则
在确定键值生成规则后,ForEach的第二个参数itemGenerator函数会根据键值生成规则为数据源的每个数组项创建组件。组件的创建包括两种情况:ForEach首次渲染和ForEach非首次渲染。
2.2.1. 首次渲染
在ForEach首次渲染时,会根据前述键值生成规则为数据源的每个数组项生成唯一键值,并创建相应的组件。
2.2.2. 非首次渲染
在ForEach组件进行非首次渲染时,它会检查新生成的键值是否在上次渲染中已经存在。如果键值不存在,则会创建一个新的组件;如果键值存在,则不会创建新的组件,而是直接渲染该键值所对应的组件。
2.3. 使用建议
-
尽量避免在最终的键值生成规则中包含数据项索引index,以防止出现渲染结果非预期和渲染性能降低。如果业务确实需要使用index,例如列表需要通过index进行条件渲染,开发者需要接受ForEach在改变数据源后重新创建组件所带来的性能损耗。
-
为满足键值的唯一性,对于对象数据类型,建议使用对象数据中的唯一id作为键值。
-
基本数据类型的数据项没有唯一ID属性。如果使用基本数据类型本身作为键值,必须确保数组项无重复。因此,对于数据源会发生变化的场景,建议将基本数据类型数组转化为具备唯一ID属性的对象数据类型数组,再使用ID属性作为键值生成规则。
-
开发者在使用ForEach时应尽量避免最终键值生成规则中包含index。
3. LazyForEach:懒加载渲染
LazyForEach从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。当在滚动容器中使用了LazyForEach,框架会根据滚动容器可视区域按需创建组件,当组件滑出可视区域外时,框架会进行组件销毁回收以降低内存占用。
用法:
LazyForEach(dataSource: IDataSource, // 需要进行数据迭代的数据源itemGenerator: (item: any, index: number) => void, // 子组件生成函数keyGenerator?: (item: any, index: number) => string // 键值生成函数
): void
// IDataSource 需要提供给 LazyForEach 的数据源 必须要实现的接口
class BasicDataSource implements IDataSource {private listeners: DataChangeListener[] = new Array<DataChangeListener>();
public totalCount(): number {return 0;}
public getData(index: number): Object {return 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);})}
notifyDataMove(from: number, to: number): void {this.listeners.forEach(listener => {listener.onDataMove(from, to);})}
}
// 将内部操纵数据的方法 类型,替换为我们希望的数据
class MyDataSource extends BasicDataSource {// 数据本身private dataArray: MkGoodsItem[] = [];
// 总个数public totalCount(): number {return this.dataArray.length;}
// 获取数据public getData(index: number): Object {return this.dataArray[index];}
// 在指定的位置添加数据public addData(index: number, data: MkGoodsItem): void {this.dataArray.splice(index, 0, data);this.notifyDataAdd(index);}
// 最佳数据public pushData(data: MkGoodsItem): void {this.dataArray.push(data);// notify 通知// DataAdd 添加数据// 通知数据源 数据背添加了this.notifyDataAdd(this.dataArray.length - 1);}
// 清空数据public clear(): void {this.dataArray = [];}
}
4. 总结
4.1. 条件控制渲染:
-
介绍:条件控制渲染语句运行直接在ArkUI容器组件内使用,通过条件渲染构建不同的子组件。
-
机制:每个分支包含一个构建函数,会创建一个或多个子组件;当使用的状态变量发生变化后会卸载调之前的节点组件,重新构建另外的分支的组件。
-
注意:在某些容器组件限制子组件的类型或数量的,条件渲染语句也会受限制,Grid组件内仅支持GridItem子组件;List组件只支持ListItem子组件;Scoll组件只能有一个根组件。
4.2. forEach循环渲染:
-
介绍:和数组中的forEach方法原理类似,但是可以直接在ArkUI容器组件中使用,通过循环的形式创建多个子组件。
-
机制:
-
在ArkTS中,
forEach
有三个参数,分别是arr
、itemGenerator
和keyGenerator
。各参数的含义如下: -
arr
:需要进行循环渲染的数据源,必须为数组类型。 -
itemGenerator
:组件生成函数,用于为arr
数组中的每个元素创建对应的组件。该函数可接收两个参数,分别是:-
item
:arr
数组中的数据项。 -
index`(可选) : arr数组中的数据项的索引。
-
-
keyGenerator
(可选):key
生成函数,用于为arr
数组中的每个数据项生成唯一的key 。key
的作用是辅助forEach
完成组件对象的复用。
-
-
注意:
-
使用ForEach时应尽量避免最终键值生成规则中包含index。大白话:第二个参数和第三个参数要同步使用。
-
如果数组id发生变化,比如某个元素被添加或者删除会导致索引乱序。需要使用三个参数来确定唯一id。
-
4.3. LazyForEach懒加载渲染:
-
介绍:LazyForEach从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。当在滚动容器中使用了LazyForEach,框架会根据滚动容器可视区域按需创建组件,当组件滑出可视区域外时,框架会进行组件销毁回收以降低内存占用。
-
机制:键值生成规则还有组件创建规则和ForEach渲染的机制差不多,不同的是数据源,通过实现内置接口IDataSource来监听注册数据的改变从而在每次迭代中只创建一个组件,并卸载之前的组件,达到降低内存的效果。
-
注意:
-
只能在特定的支持懒加载的组件中使用,比如List、Grid、Swiper、WaterFlow等。
-
优化效果好,对一些长列表组件,轮播图等,特别有用。
-
4.4. LazyForEach和forEach的区别
在鸿蒙开发中,LazyForEach和forEach的主要区别在于它们的迭代方式和性能。 forEach 会立即迭代数组中的所有元素,并在每次迭代时执行提供的函数。这意味着无论元素是否实际被使用,都会进行迭代。 LazyForEach则采用惰性迭代的方式。它只会在需要时才迭代数组中的元素,即在元素被实际使用时才进行迭代。这种方式可以提高性能,特别是在处理大型数组或复杂的数据结构时,因为它避免了不必要的迭代操作。总的来说,LazyForEach更适合用于需要高性能和惰性计算的场景,而forEach则更适合用于简单的迭代操作。