OpenHarmony应用开发-学习ArkTS语言-渲染控制与使用限制与扩展
版本:v3.2 Beta5
渲染控制
ArkTS也提供了渲染控制的能力。条件渲染可根据应用的不同状态,渲染对应状态下的UI内容。循环渲染可从数据源中迭代获取数据,并在每次迭代过程中创建相应的组件。
条件渲染
使用if/else进行条件渲染。
说明:
- if/else条件语句可以使用状态变量。
- 使用if/else可以使子组件的渲染依赖条件语句。
- 必须在容器组件内使用。
- 某些容器组件限制子组件的类型或数量,将if/else用于这些组件内时,这些限制将同样应用于if/else语句内创建的组件。例如,Grid容器组件的子组件仅支持GridItem组件,在Grid内使用if/else时,则if/else语句内也仅允许使用GridItem组件。
Column() {
if (this.count < 0) {
Text('count is negative').fontSize(14)
} else if (this.count % 2 === 0) {
Text('count is even').fontSize(14)
} else {
Text('count is odd').fontSize(14)
}
}
循环渲染
通过循环渲染(ForEach)从数组中获取数据,并为每个数据项创建相应的组件,可减少代码复杂度。
ForEach(
arr: any[],
itemGenerator: (item: any, index?: number) => void,
keyGenerator?: (item: any, index?: number) => string
)
参数:
参数名 | 参数类型 | 必填 | 参数描述 |
arr | any[] | 是 | 必须是数组,允许设置为空数组,空数组场景下将不会创建子组件。同时允许设置返回值为数组类型的函数,例如arr.slice(1, 3),设置的函数不得改变包括数组本身在内的任何状态变量,如Array.splice、Array.sort或Array.reverse这些改变原数组的函数。 |
itemGenerator | (item: any, index?: number) => void | 是 | 生成子组件的lambda函数,为数组中的每一个数据项创建一个或多个子组件,单个子组件或子组件列表必须包括在大括号“{…}”中。 |
keyGenerator | (item: any, index?: number) => string | 否 | 匿名函数,用于给数组中的每一个数据项生成唯一且固定的键值。当数据项在数组中的位置更改时,其键值不得更改,当数组中的数据项被新项替换时,被替换项的键值和新项的键值必须不同。键值生成器的功能是可选的,但是,为了使开发框架能够更好地识别数组更改,提高性能,建议提供。如将数组反向时,如果没有提供键值生成器,则ForEach中的所有节点都将重建。 |
说明:
- ForEach必须在容器组件内使用。
- 生成的子组件应当是允许包含在ForEach父容器组件中的子组件。
- 允许子组件生成器函数中包含if/else条件渲染,同时也允许ForEach包含在if/else条件渲染语句中。
- itemGenerator函数的调用顺序不一定和数组中的数据项相同,在开发过程中不要假设itemGenerator和keyGenerator函数是否执行及其执行顺序。例如,以下示例可能无法正常工作:
ForEach(anArray.map((item1, index1) => { return { i: index1 + 1, data: item1 }; }),
item => Text(`${item.i}. item.data.label`),
item => item.data.id.toString())
示例
// xxx.ets
@Entry
@Component
struct MyComponent {
@State arr: number[] = [10, 20, 30]
build() {
Column({ space: 5 }) {
Button('Reverse Array')
.onClick(() => {
this.arr.reverse()
})
ForEach(this.arr, (item: number) => {
Text(`item value: ${item}`).fontSize(18)
Divider().strokeWidth(2)
}, (item: number) => item.toString())
}
}
}
数据懒加载
通过数据懒加载(LazyForEach)从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。
LazyForEach(
dataSource: IDataSource,
itemGenerator: (item: any) => void,
keyGenerator?: (item: any) => string
): void
interface IDataSource {
totalCount(): number;
getData(index: number): any;
registerDataChangeListener(listener: DataChangeListener): void;
unregisterDataChangeListener(listener: DataChangeListener): void;
}
interface DataChangeListener {
onDataReloaded(): void;
onDataAdd(index: number): void;
onDataMove(from: number, to: number): void;
onDataDelete(index: number): void;
onDataChange(index: number): void;
}
参数:
参数名 | 参数类型 | 必填 | 参数描述 |
dataSource | IDataSource | 是 | 实现IDataSource接口的对象,需要开发者实现相关接口。 |
itemGenerator | (item: any, index?: number) => void | 是 | 生成子组件的lambda函数,为数组中的每一个数据项创建一个或多个子组件,单个子组件或子组件列表必须包括在大括号“{…}”中。 |
keyGenerator | (item: any, index?: number) => string | 否 | 匿名函数,用于给数组中的每一个数据项生成唯一且固定的键值。当数据项在数组中的位置更改时,其键值不得更改,当数组中的数据项被新项替换时,被替换项的键值和新项的键值必须不同。键值生成器的功能是可选的,但是,为了使开发框架能够更好地识别数组更改,提高性能,建议提供。如将数组反向时,如果没有提供键值生成器,则LazyForEach中的所有节点都将重建。 |
IDataSource类型说明
名称 | 描述 |
totalCount(): number | 获取数据总数。 |
getData(index: number): any | 获取索引值index对应的数据。 |
registerDataChangeListener(listener:DataChangeListener): void | 注册数据改变的监听器。 |
unregisterDataChangeListener(listener:DataChangeListener): void | 注销数据改变的监听器。 |
DataChangeListener类型说明
名称 | 描述 |
onDataReloaded(): void | 重新加载所有数据。 |
onDataAdded(index: number): voiddeprecated | 通知组件index的位置有数据添加。从API Version 8开始废弃,建议使用onDataAdd。 |
onDataMoved(from: number, to: number): voiddeprecated | 通知组件数据从from的位置移到to的位置。从API Version 8开始废弃,建议使用onDataMove。 |
onDataDeleted(index: number): voiddeprecated | 通知组件index的位置有数据删除。从API Version 8开始废弃,建议使用onDataDelete。 |
onDataChanged(index: number): voiddeprecated | 通知组件index的位置有数据变化。 从API Version 8开始废弃,建议使用onDataChange。 |
onDataAdd(index: number): void8+ | 通知组件index的位置有数据添加。 |
onDataMove(from: number, to: number): void8+ | 通知组件数据从from的位置移到to的位置。 |
onDataDelete(index: number): void8+ | 通知组件index的位置有数据删除。 |
onDataChange(index: number): void8+ | 通知组件index的位置有数据变化。 |
示例
// xxx.ets
class BasicDataSource implements IDataSource {
private listeners: DataChangeListener[] = []
public totalCount(): number {
return 0
}
public getData(index: number): any {
return undefined
}
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
console.info('add listener')
this.listeners.push(listener)
}
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
console.info('remove listener')
this.listeners.splice(pos, 1)
}
}
notifyDataReload(): void {
this.listeners.forEach(listener => {
listener.onDataReloaded()
})
}
notifyDataAdd(index: number): void {
this.listeners.forEach(listener => {
listener.onDataAdd(index)
})
}
notifyDataChange(index: number): void {
this.listeners.forEach(listener => {
listener.onDataChange(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: string[] = ['/path/image0.png', '/path/image1.png', '/path/image2.png', '/path/image3.png']
public totalCount(): number {
return this.dataArray.length
}
public getData(index: number): any {
return this.dataArray[index]
}
public addData(index: number, data: string): void {
this.dataArray.splice(index, 0, data)
this.notifyDataAdd(index)
}
public pushData(data: string): void {
this.dataArray.push(data)
this.notifyDataAdd(this.dataArray.length - 1)
}
}
@Entry
@Component
struct MyComponent {
private data: MyDataSource = new MyDataSource()
build() {
List({ space: 3 }) {
LazyForEach(this.data, (item: string) => {
ListItem() {
Row() {
Image(item).width(50).height(50)
Text(item).fontSize(20).margin({ left: 10 })
}.margin({ left: 10, right: 10 })
}
.onClick(() => {
// 每点击一次列表项,数据增加一项
this.data.pushData('/path/image' + this.data.totalCount() + '.png')
})
}, item => item)
}.height('100%').width('100%')
}
}
说明:
- LazyForEach必须在容器组件内使用,目前仅有List、Grid以及Swiper组件支持数据懒加载(即只加载可视部分以及其前后少量数据用于缓冲),其他组件仍然是一次性加载所有的数据。
- LazyForEach在每次迭代中,必须创建且只允许创建一个子组件。
- 生成的子组件必须是允许包含在LazyForEach父容器组件中的子组件。
- 允许LazyForEach包含在if/else条件渲染语句中。
- 为了高性能渲染,通过DataChangeListener对象的onDataChange方法来更新UI时,仅当itemGenerator中创建的子组件内使用了状态变量时,才会触发组件刷新。
- itemGenerator函数的调用顺序不一定和数据源中的数据项相同,在开发过程中不要假设itemGenerator和keyGenerator函数是否执行及其执行顺序。例如,以下示例可能无法正常工作:
LazyForEach(dataSource,
item => Text(`${item.i}. item.data.label`),
item => item.data.id.toString())
使用限制与扩展
在生成器函数中的使用限制
ArkTS语言的使用在生成器函数中存在一定的限制:
- 表达式仅允许在字符串(${expression})、if/else条件语句、ForEach的参数以及组件的参数中使用。
- 任何表达式都不能导致应用程序中状态变量(@State、@Link、@Prop)的改变,否则会造成未定义和潜在不稳定的框架行为。
- 生成器函数内部不能有局部变量。
上述限制都不适用于事件方法(如onClick)的匿名函数实现。
变量的双向绑定
ArkTS支持通过$$双向绑定变量,通常应用于状态值频繁改变的变量。
- 当前$$支持基础类型变量,以及@State、@Link和@Prop装饰的变量。
- 当前$$仅支持bindPopup属性方法的show参数,Radio组件的checked属性,Refresh组件的refreshing参数。
- $$绑定的变量变化时,仅渲染刷新当前组件,提高渲染速度。
// xxx.ets
@Entry
@Component
struct bindPopupPage {
@State customPopup: boolean = false
build() {
Column() {
Button('Popup')
.margin(20)
.onClick(() => {
this.customPopup = !this.customPopup
})
.bindPopup($$this.customPopup, {
message: "showPopup"
})
}
}
}
状态变量数据类型声明使用限制
- 所有的状态装饰器变量需要显式声明变量类型,不允许声明any,不支持Date数据类型。
示例:
// xxx.ets
@Entry
@Component
struct DatePickerExample {
//错误写法: @State isLunar: any = false
@State isLunar: boolean = false
//错误写法: @State selectedDate: Date = new Date('2021-08-08')
private selectedDate: Date = new Date('2021-08-08')
build() {
Column() {
Button('切换公历农历')
.margin({ top: 30 })
.onClick(() => {
this.isLunar = !this.isLunar
})
DatePicker({
start: new Date('1970-1-1'),
end: new Date('2100-1-1'),
selected: this.selectedDate
})
.lunar(this.isLunar)
.onChange((value: DatePickerResult) => {
this.selectedDate.setFullYear(value.year, value.month, value.day)
console.info('select current date is: ' + JSON.stringify(value))
})
}.width('100%')
}
}
- @State、@Provide、 @Link和@Consume四种状态变量的数据类型声明只能由简单数据类型或引用数据类型的其中一种构成。
类型定义中的Length、ResourceStr、ResourceColor三个类型是简单数据类型或引用数据类型的组合,所以不能被以上四种状态装饰器变量使用。 Length、ResourceStr、ResourceColor的定义请看文档arkui-ts类型定义。
示例:
// xxx.ets
@Entry
@Component
struct IndexPage {
//错误写法: @State message: string | Resource = 'Hello World'
@State message: string = 'Hello World'
//错误写法: @State message: ResourceStr = $r('app.string.hello')
@State resourceStr: Resource = $r('app.string.hello')
build() {
Row() {
Column() {
Text(`${this.message}`)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
.width('100%')
}
.height('100%')
}
}
自定义组件成员变量初始化的方式与约束
组件的成员变量可以通过两种方式初始化:
- 本地初始化:
@State counter: Counter = new Counter()
- 在构造组件时通过构造参数初始化:
MyComponent({counter: $myCounter})
具体允许哪种方式取决于状态变量的装饰器:
装饰器类型 | 本地初始化 | 通过构造函数参数初始化 |
@State | 必须 | 可选 |
@Prop | 禁止 | 必须 |
@Link | 禁止 | 必须 |
@StorageLink | 必须 | 禁止 |
@StorageProp | 必须 | 禁止 |
@LocalStorageLink | 必须 | 禁止 |
@LocalStorageProp | 必须 | 禁止 |
@Provide | 必须 | 可选 |
@Consume | 禁止 | 禁止 |
@ObjectLink | 禁止 | 必须 |
常规成员变量 | 推荐 | 可选 |
从上表中可以看出:
- @State变量需要本地初始化,初始化的值可以被构造参数覆盖。
- @Prop和@Link变量必须且仅通过构造函数参数进行初始化。
通过构造函数方法初始化成员变量,需要遵循如下规则:
从父组件中的变量(右)到子组件中的变量(下) | regular | @State | @Link | @Prop | @Provide | @Consume | @ObjectLink |
regular | 支持 | 支持 | 支持 | 支持 | 不支持 | 不支持 | 支持 |
@State | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 |
@Link | 不支持 | 支持(1) | 支持(1) | 支持(1) | 支持(1) | 支持(1) | 支持(1) |
@Prop | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 |
@Provide | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 |
@Consume | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 |
@ObjectLink | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 |
从父组件中的变量(右)到子组件中的变量(下) | @StorageLink | @StorageProp | @LocalStorageLink | @LocalStorageProp |
regular | 支持 | 不支持 | 不支持 | 不支持 |
@State | 支持 | 支持 | 支持 | 支持 |
@Link | 支持(1) | 支持(1) | 支持(1) | 支持(1) |
@Prop | 支持 | 支持 | 支持 | 支持 |
@Provide | 支持 | 支持 | 支持 | 支持 |
@Consume | 不支持 | 不支持 | 不支持 | 不支持 |
@ObjectLink | 不支持 | 不支持 | 不支持 | 不支持 |
说明
支持(1):必须使用
$
, 例如 this.$varA
。
regular:未加修饰的常规变量。
从上表中可以看出:
- 子组件的@ObjectLink变量不支持父组件装饰器变量的直接赋值,其父组件的源必须是数组的项或对象的属性,该数组或对象必现用
@State
、@Link
、@Provide
、@Consume
或@ObjectLink
装饰器修饰。 - 父组件的常规变量可以用于初始化子组件的
@State
变量,但不能用于初始化@Link
、@Consume
和@ObjectLink
变量。 - 父组件的@State变量可以初始化子组件的
@Prop
、@Link
(通过$)或常规变量,但不能初始化子组件的@Consume变量。 - 父组件的@Link变量不可以初始化子组件的
@Consume
和@ObjectLink
变量。 - 父组件的@Prop变量不可以初始化子组件的
@Consume
和@ObjectLink
变量。 - 不允许从父组件初始化
@StorageLink
,@StorageProp
,@LocalStorageLink
,@LocalStorageProp
修饰的变量。 - 除了上述规则外,还需要遵循TS的强类型规则。
示例:
@Entry
@Component
struct Parent {
message: string = "Hello World"
build() {
Column() {
Child({
stateMessage: this.message,
/* ArkTS:ERROR The regular property 'message' cannot be assigned
to the @Link property 'linkMessage'.*/
linkMessage: this.$message
})
}
.width('100%')
}
}
@Component
struct Child {
@State stateMessage: string = "Hello World"
@Link linkMessage: string
build() {
Column() {
Text(this.stateMessage)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
.width('100%')
}
}