渲染控制概述
ArkUI通过自定义组件的build()函数和@builder装饰器中的声明式UI描述语句构建相应的UI。在声明式描述语句中开发者除了使用系统组件外,还可以使用渲染控制语句来辅助UI的构建,这些渲染控制语句包括控制组件是否显示的条件渲染语句,基于数组数据快速生成组件的循环渲染语句以及针对大数据量场景的数据懒加载语句。
总结起来,就是HarmonyOS有三种渲染控制:
- if/else:条件渲染
- ForEach:循环渲染
- LazyForEach:数据懒加载
废话不多说,今天我们就来讲讲这三种渲染控制
条件渲染( if/else)
ArkTS提供了渲染控制的能力。条件渲染可根据应用的不同状态,使用if、else和else if渲染对应状态下的UI内容。
注意⚠️:从API version 9开始,该接口支持在ArkTS卡片中使用。
1.使用规则
条件渲染的使用要遵循以下规则:
- 支持if、else和else if语句。
- if、else if后跟随的条件语句可以使用状态变量。
- 允许在容器组件内使用,通过条件渲染语句构建不同的子组件。
- 条件渲染语句在涉及到组件的父子关系时是“透明”的,当父组件和子组件之间存在一个或多个if语句时,必须遵守父组件关于子组件使用的规则。
- 每个分支内部的构建函数必须遵循构建函数的规则,并创建一个或多个组件。无法创建组件的空构建函数会产生语法错误。
- 某些容器组件限制子组件的类型或数量,将条件渲染语句用于这些组件内时,这些限制将同样应用于条件渲染语句内创建的组件。例如,Grid容器组件的子组件仅支持GridItem组件,在Grid内使用条件渲染语句时,条件渲染语句内仅允许使用GridItem组件。
2.更新机制
当if、else if后跟随的状态判断中使用的状态变量值变化时,条件渲染语句会进行更新,更新步骤如下:
-
评估if和else if的状态判断条件,如果分支没有变化,无需执行以下步骤。如果分支有变化,则执行2、3步骤:
-
删除此前构建的所有子组件。
-
执行新分支的构造函数,将获取到的组件添加到if父容器中。如果缺少适用的else分支,则不构建任何内容。
条件可以包括Typescript表达式。对于构造函数中的表达式,此类表达式不得更改应用程序状态。
3.使用场景
1.使用if进行条件渲染
@Entry
@Component
struct ViewA {@State count: number = 0;build() {Column() {Text(`count=${this.count}`)if (this.count > 0) {Text(`count is positive`).fontColor(Color.Green)}Button('increase count').onClick(() => {this.count++;})Button('decrease count').onClick(() => {this.count--;})}}
}
if语句的每个分支都包含一个构建函数。此类构建函数必须创建一个或多个子组件。在初始渲染时,if语句会执行构建函数,并将生成的子组件添加到其父组件中。
每当if或else if条件语句中使用的状态变量发生变化时,条件语句都会更新并重新评估新的条件值。如果条件值评估发生了变化,这意味着需要构建另一个条件分支。此时ArkUI框架将:
- 删除所有以前渲染的(早期分支的)组件。
- 执行新分支的构造函数,将生成的子组件添加到其父组件中。
在以上示例中,如果count从0增加到1,那么if语句更新,条件count > 0将重新评估,评估结果将从false更改为true。因此,将执行条件为真分支的构造函数,创建一个Text组件,并将它添加到父组件Column中。如果后续count更改为0,则Text组件将从Column组件中删除。由于没有else分支,因此不会执行新的构造函数。
2.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;})}}
}
CounterView(label为 ‘CounterView #positive’)子组件在初次渲染时创建。此子组件携带名为counter的状态变量。当修改CounterView.counter状态变量时,CounterView(label为 ‘CounterView #positive’)子组件重新渲染时并保留状态变量值。当MainView.toggle状态变量的值更改为false时,MainView父组件内的if语句将更新,随后将删除CounterView(label为 ‘CounterView #positive’)子组件。与此同时,将创建新的CounterView(label为 ‘CounterView #negative’)实例。而它自己的counter状态变量设置为初始值0。
注意⚠️:CounterView(label为 ‘CounterView #positive’)和CounterView(label为 ‘CounterView #negative’)是同一自定义组件的两个不同实例。if分支的更改,不会更新现有子组件,也不会保留状态。
以下示例展示了条件更改时,若需要保留counter值所做的修改。
@Component
struct CounterView {@Link counter: number;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;@State counter: number = 0;build() {Column() {if (this.toggle) {CounterView({ counter: $counter, label: 'CounterView #positive' })} else {CounterView({ counter: $counter, label: 'CounterView #negative' })}Button(`toggle ${this.toggle}`).onClick(() => {this.toggle = !this.toggle;})}}
}
此处,@State counter变量归父组件所有。因此,当CounterView组件实例被删除时,该变量不会被销毁。CounterView组件通过@Link装饰器引用状态。状态必须从子级移动到其父级(或父级的父级),以避免在条件内容或重复内容被销毁时丢失状态。
3.嵌套if语句
举个例子:
@Entry
@Component
struct CompA {@State toggle: boolean = false;@State toggleColor: boolean = false;build() {Column() {Text('Before').fontSize(15)if (this.toggle) {Text('Top True, positive 1 top').backgroundColor('#aaffaa').fontSize(20)// 内部if语句if (this.toggleColor) {Text('Top True, Nested True, positive COLOR Nested ').backgroundColor('#00aaaa').fontSize(15)} else {Text('Top True, Nested False, Negative COLOR Nested ').backgroundColor('#aaaaff').fontSize(15)}} else {Text('Top false, negative top level').fontSize(20).backgroundColor('#ffaaaa')if (this.toggleColor) {Text('positive COLOR Nested ').backgroundColor('#00aaaa').fontSize(15)} else {Text('Negative COLOR Nested ').backgroundColor('#aaaaff').fontSize(15)}}Text('After').fontSize(15)Button('Toggle Outer').onClick(() => {this.toggle = !this.toggle;})Button('Toggle Inner').onClick(() => {this.toggleColor = !this.toggleColor;})}}
}
条件语句的嵌套对父组件的相关规则没有影响。
循环渲染( ForEach)
ForEach是一个接口,不同于条件渲染( if/else)。ForEach接口基于数组类型数据来进行循环渲染,需要与容器组件配合使用,且接口返回的组件应当是允许包含在ForEach父容器组件中的子组件。例如,ListItem组件要求ForEach的父容器组件必须为List组件。
注意⚠️:从API version 9开始,该接口支持在ArkTS卡片中使用。
1.ForEach接口说明
看一下接口参数组成:
ForEach(arr: Array,itemGenerator: (item: any, index?: number) => void,keyGenerator?: (item: any, index?: number): string => string
)
对接口各个参数做一下简单说明:
参数名称 | 参数类型 | 是否必填 | 参数描述 |
---|---|---|---|
arr | Array | 是 | 数据源,为Array类型的数组。说明:1: 可以设置为空数组,此时不会创建子组件。2:可以设置返回值为数组类型的函数,例如arr.slice(1, 3),但设置的函数不应改变包括数组本身在内的任何状态变量,例如不应使用Array.splice(),Array.sort()或Array.reverse()这些会改变原数组的函数。 |
itemGenerator | (item: any, index?: number) => void | 是 | 组件生成函数。1. 为数组中的每个元素创建对应的组件。2. item参数:arr数组中的数据项。3. index参数(可选):arr数组中的数据项索引。注意:组件的类型必须是ForEach的父容器所允许的。例如,ListItem组件要求ForEach的父容器组件必须为List组件。 |
keyGenerator | (item: any, index?: number) => string | 否 | 键值生成函数。1. 为数据源arr的每个数组项生成唯一且持久的键值。函数返回值为开发者自定义的键值生成规则。2. item参数:arr数组中的数据项。- index参数(可选):arr数组中的数据项索引。说明:1. 如果函数缺省,框架默认的键值生成函数为(item: T, index: number) => { return index + ‘__’ + JSON.stringify(item); } 2. 键值生成函数不应改变任何组件状态。 |
说明⚠️
- ForEach的itemGenerator函数可以包含if/else条件渲染逻辑。另外,也可以在if/else条件渲染语句中使用ForEach组件。
- 在初始化渲染时,ForEach会加载数据源的所有数据,并为每个数据项创建对应的组件,然后将其挂载到渲染树上。如果数据源非常大或有特定的性能需求,建议使用LazyForEach组件。
2.键值生成规则
在ForEach循环渲染过程中,系统会为每个数组元素生成一个唯一且持久的键值,用于标识对应的组件。当这个键值变化时,ArkUI框架将视为该数组元素已被替换或修改,并会基于新的键值创建一个新的组件。
ForEach提供了一个名为keyGenerator的参数,这是一个函数,开发者可以通过它自定义键值的生成规则。如果开发者没有定义keyGenerator函数,则ArkUI框架会使用默认的键值生成函数,即(item: any, index: number) => { return index + ‘__’ + JSON.stringify(item); }。
ArkUI框架对于ForEach的键值生成有一套特定的判断规则,这主要与itemGenerator函数的第二个参数index以及keyGenerator函数的返回值有关。总的来说,只有当开发者在itemGenerator函数中声明了index参数,并且自定义的keyGenerator函数返回值中不包含index参数时,ArkUI框架才会在开发者自定义的keyGenerator函数返回值前添加index参数,作为最终的键值。在其他情况下,系统将直接使用开发者自定义的keyGenerator函数返回值作为最终的键值。如果keyGenerator函数未定义,系统将使用上述默认的键值生成函数。具体的键值生成规则判断逻辑如下图所示。
注意⚠️:ArkUI框架会对重复的键值发出警告。在UI更新的场景下,如果出现重复的键值,框架可能无法正常工作,具体请参见渲染结果非预期。
3.组件创建规则
在确定键值生成规则后,ForEach的第二个参数itemGenerator函数会根据键值生成规则为数据源的每个数组项创建组件。组件的创建包括两种情况:首次渲染和非首次渲染。
1.首次渲染
在ForEach首次渲染时,会根据前述键值生成规则为数据源的每个数组项生成唯一键值,并创建相应的组件。
@Entry
@Component
struct Parent {@State simpleList: Array<string> = ['one', 'two', 'three'];build() {Row() {Column() {ForEach(this.simpleList, (item: string) => {ChildItem({ item: item })}, (item: string) => item)}.width('100%').height('100%')}.height('100%').backgroundColor(0xF1F3F5)}
}@Component
struct ChildItem {@Prop item: string;build() {Text(this.item).fontSize(50)}
}
运行效果如下:
在上述代码中,键值生成规则是keyGenerator函数的返回值item。在ForEach渲染循环时,为数据源数组项依次生成键值one、two和three,并创建对应的ChildItem组件渲染到界面上。
当不同数组项按照键值生成规则生成的键值相同时,框架的行为是未定义的。例如,在以下代码中,ForEach渲染相同的数据项two时,只创建了一个ChildItem组件,而没有创建多个具有相同键值的组件。
@Entry
@Component
struct Parent {@State simpleList: Array<string> = ['one', 'two', 'two', 'three'];build() {Row() {Column() {ForEach(this.simpleList, (item: string) => {ChildItem({ item: item })}, (item: string) => item)}.width('100%').height('100%')}.height('100%').backgroundColor(0xF1F3F5)}
}@Component
struct ChildItem {@Prop item: string;build() {Text(this.item).fontSize(50)}
}
运行ForEach数据源存在相同值案例首次渲染运行效果图
在该示例中,最终键值生成规则为item。当ForEach遍历数据源simpleList,遍历到索引为1的two时,按照最终键值生成规则生成键值为two的组件并进行标记。当遍历到索引为2的two时,按照最终键值生成规则当前项的键值也为two,此时不再创建新的组件。
2.非首次渲染
在ForEach组件进行非首次渲染时,它会检查新生成的键值是否在上次渲染中已经存在。如果键值不存在,则会创建一个新的组件;如果键值存在,则不会创建新的组件,而是直接渲染该键值所对应的组件。例如,在以下的代码示例中,通过点击事件修改了数组的第三项值为"new three",这将触发ForEach组件进行非首次渲染。
@Entry
@Component
struct Parent {@State simpleList: Array<string> = ['one', 'two', 'three'];build() {Row() {Column() {Text('点击修改第3个数组项的值').fontSize(24).fontColor(Color.Red).onClick(() => {this.simpleList[2] = 'new three';})ForEach(this.simpleList, (item: string) => {ChildItem({ item: item }).margin({ top: 20 })}, (item: string) => item)}.justifyContent(FlexAlign.Center).width('100%').height('100%')}.height('100%').backgroundColor(0xF1F3F5)}
}@Component
struct ChildItem {@Prop item: string;build() {Text(this.item).fontSize(30)}
}
运行ForEach非首次渲染案例运行效果图
从本例可以看出@State 能够监听到简单数据类型数组数据源 simpleList 数组项的变化。
- 当 simpleList 数组项发生变化时,会触发 ForEach 进行重新渲染。
- ForEach 遍历新的数据源 [‘one’, ‘two’, ‘new three’],并生成对应的键值one、two和new three。
- 其中,键值one和two在上次渲染中已经存在,所以 ForEach 复用了对应的组件并进行了渲染。对于第三个数组项 “new three”,由于其通过键值生成规则 item 生成的键值new three在上次渲染中不存在,因此 ForEach 为该数组项创建了一个新的组件。
4.使用场景
ForEach组件在开发过程中的主要应用场景包括:数据源不变、数据源数组项发生变化(如插入、删除操作)、数据源数组项子属性变化。
1.数据源不变
在数据源保持不变的场景中,数据源可以直接采用基本数据类型。例如,在页面加载状态时,可以使用骨架屏列表进行渲染展示。
@Entry
@Component
struct ArticleList {@State simpleList: Array<number> = [1, 2, 3, 4, 5];build() {Column() {ForEach(this.simpleList, (item: string) => {ArticleSkeletonView().margin({ top: 20 })}, (item: string) => item)}.padding(20).width('100%').height('100%')}
}@Builder
function textArea(width: number | Resource | string = '100%', height: number | Resource | string = '100%') {Row().width(width).height(height).backgroundColor('#FFF2F3F4')
}@Component
struct ArticleSkeletonView {build() {Row() {Column() {textArea(80, 80)}.margin({ right: 20 })Column() {textArea('60%', 20)textArea('50%', 20)}.alignItems(HorizontalAlign.Start).justifyContent(FlexAlign.SpaceAround).height('100%')}.padding(20).borderRadius(12).backgroundColor('#FFECECEC').height(120).width('100%').justifyContent(FlexAlign.SpaceBetween)}
}
运行效果如下图所示:
在本示例中,采用数据项item作为键值生成规则,由于数据源simpleList的数组项各不相同,因此能够保证键值的唯一性。
2.数据源数组项发生变化
在数据源数组项发生变化的场景下,例如进行数组插入、删除操作或者数组项索引位置发生交换时,数据源应为对象数组类型,并使用对象的唯一ID作为最终键值。例如,当在页面上通过手势上滑加载下一页数据时,会在数据源数组尾部新增新获取的数据项,从而使得数据源数组长度增大。
@Entry
@Component
struct ArticleListView {@State isListReachEnd: boolean = false;@State articleList: Array<Article> = [new Article('001', '第1篇文章', '文章简介内容'),new Article('002', '第2篇文章', '文章简介内容'),new Article('003', '第3篇文章', '文章简介内容'),new Article('004', '第4篇文章', '文章简介内容'),new Article('005', '第5篇文章', '文章简介内容'),new Article('006', '第6篇文章', '文章简介内容')]loadMoreArticles() {this.articleList.push(new Article('007', '加载的新文章', '文章简介内容'));}build() {Column({ space: 5 }) {List() {ForEach(this.articleList, (item: Article) => {ListItem() {ArticleCard({ article: item }).margin({ top: 20 })}}, (item: Article) => item.id)}.onReachEnd(() => {this.isListReachEnd = true;}).parallelGesture(PanGesture({ direction: PanDirection.Up, distance: 80 }).onActionStart(() => {if (this.isListReachEnd) {this.loadMoreArticles();this.isListReachEnd = false;}})).padding(20).scrollBar(BarState.Off)}.width('100%').height('100%').backgroundColor(0xF1F3F5)}
}@Component
struct ArticleCard {@Prop article: Article;build() {Row() {Image($r('app.media.icon')).width(80).height(80).margin({ right: 20 })Column() {Text(this.article.title).fontSize(20).margin({ bottom: 8 })Text(this.article.brief).fontSize(16).fontColor(Color.Gray).margin({ bottom: 8 })}.alignItems(HorizontalAlign.Start).width('80%').height('100%')}.padding(20).borderRadius(12).backgroundColor('#FFECECEC').height(120).width('100%').justifyContent(FlexAlign.SpaceBetween)}
}
初始运行效果(左图)和手势上滑加载后效果(右图)如下图所示。
在本示例中,ArticleCard组件作为ArticleListView组件的子组件,通过@Prop装饰器接收一个Article对象,用于渲染文章卡片。
- 当列表滚动到底部时,如果手势滑动距离超过指定的80,将触发loadMoreArticle()函数。此函数会在articleList数据源的尾部添加一个新的数据项,从而增加数据源的长度。
- 数据源被@State装饰器修饰,ArkUI框架能够感知到数据源长度的变化,并触发ForEach进行重新渲染。
3.数据源数组项子属性变化
当数据源的数组项为对象数据类型,并且只修改某个数组项的属性值时,由于数据源为复杂数据类型,ArkUI框架无法监听到@State装饰器修饰的数据源数组项的属性变化,从而无法触发ForEach的重新渲染。为实现ForEach重新渲染,需要结合@Observed和@ObjectLink装饰器使用。例如,在文章列表卡片上点击“点赞”按钮,从而修改文章的点赞数量。
@Entry
@Component
struct ArticleListView {@State articleList: Array<Article> = [new Article('001', '第0篇文章', '文章简介内容', false, 100),new Article('002', '第1篇文章', '文章简介内容', false, 100),new Article('003', '第2篇文章', '文章简介内容', false, 100),new Article('004', '第4篇文章', '文章简介内容', false, 100),new Article('005', '第5篇文章', '文章简介内容', false, 100),new Article('006', '第6篇文章', '文章简介内容', false, 100),];build() {List() {ForEach(this.articleList, (item: Article) => {ListItem() {ArticleCard({article: item}).margin({ top: 20 })}}, (item: Article) => item.id)}.padding(20).scrollBar(BarState.Off).backgroundColor(0xF1F3F5)}
}@Component
struct ArticleCard {@ObjectLink article: Article;handleLiked() {this.article.isLiked = !this.article.isLiked;this.article.likesCount = this.article.isLiked ? this.article.likesCount + 1 : this.article.likesCount - 1;}build() {Row() {Image($r('app.media.icon')).width(80).height(80).margin({ right: 20 })Column() {Text(this.article.title).fontSize(20).margin({ bottom: 8 })Text(this.article.brief).fontSize(16).fontColor(Color.Gray).margin({ bottom: 8 })Row() {Image(this.article.isLiked ? $r('app.media.iconLiked') : $r('app.media.iconUnLiked')).width(24).height(24).margin({ right: 8 })Text(this.article.likesCount.toString()).fontSize(16)}.onClick(() => this.handleLiked()).justifyContent(FlexAlign.Center)}.alignItems(HorizontalAlign.Start).width('80%').height('100%')}.padding(20).borderRadius(12).backgroundColor('#FFECECEC').height(120).width('100%').justifyContent(FlexAlign.SpaceBetween)}
}
上述代码的初始运行效果(左图)和点击第1个文章卡片上的点赞图标后的运行效果(右图)如下图所示。
在本示例中,Article类被@Observed装饰器修饰。父组件ArticleListView传入Article对象实例给子组件ArticleCard,子组件使用@ObjectLink装饰器接收该实例。
- 当点击第1个文章卡片上的点赞图标时,会触发ArticleCard组件的handleLiked函数。该函数修改第1个卡片对应组件里article实例的isLiked和likesCount属性值。
- 由于子组件ArticleCard中的article使用了@ObjectLink装饰器,父子组件共享同一份article数据。因此,父组件中articleList的第1个数组项的isLiked和likedCounts数值也会同步修改。
- 当父组件监听到数据源数组项属性值变化时,会触发ForEach重新渲染。
- 在此处,ForEach键值生成规则为数组项的id属性值。当ForEach遍历新数据源时,数组项的id均没有变化,不会新建组件。
- 渲染第1个数组项对应的ArticleCard组件时,读取到的isLiked和likesCount为修改后的新值。
5.使用建议
- 尽量避免在最终的键值生成规则中包含数据项索引index,以防止出现渲染结果非预期和渲染性能降低。如果业务确实需要使用index,例如列表需要通过index进行条件渲染,开发者需要接受ForEach在改变数据源后重新创建组件所带来的性能损耗。
- 为满足键值的唯一性,对于对象数据类型,建议使用对象数据中的唯一id作为键值。
- 基本数据类型的数据项没有唯一ID属性。如果使用基本数据类型本身作为键值,必须确保数组项无重复。因此,对于数据源会发生变化的场景,建议将基本数据类型数组转化为具备唯一ID属性的对象数据类型数组,再使用ID属性作为键值生成规则。
6.不推荐案例
开发者在使用ForEach的过程中,若对于键值生成规则的理解不够充分,可能会出现错误的使用方式。错误使用一方面会导致功能层面问题,例如渲染结果非预期,另一方面会导致性能层面问题,例如渲染性能降低。
1.渲染结果非预期
在本示例中,通过设置ForEach的第三个参数KeyGenerator函数,自定义键值生成规则为数据源的索引index的字符串类型值。当点击父组件Parent中“在第1项后插入新项”文本组件后,界面会出现非预期的结果。
@Entry
@Component
struct Parent {@State simpleList: Array<string> = ['one', 'two', 'three'];build() {Column() {Button() {Text('在第1项后插入新项').fontSize(30)}.onClick(() => {this.simpleList.splice(1, 0, 'new item');})ForEach(this.simpleList, (item: string) => {ChildItem({ item: item })}, (item: string, index: number) => index.toString())}.justifyContent(FlexAlign.Center).width('100%').height('100%').backgroundColor(0xF1F3F5)}
}@Component
struct ChildItem {@Prop item: string;build() {Text(this.item).fontSize(30)}
}
上述代码的初始渲染效果(左图)和点击“在第1项后插入新项”文本组件后的渲染效果(右图)如下图所示。
ForEach在首次渲染时,创建的键值依次为"0"、“1”、“2”。
插入新项后,数据源simpleList变为[‘one’, ‘new item’, ‘two’, ‘three’],框架监听到@State装饰的数据源长度变化触发ForEach重新渲染。
ForEach依次遍历新数据源,遍历数据项"one"时生成键值"0",存在相同键值,因此不创建新组件。继续遍历数据项"new item"时生成键值"1",存在相同键值,因此不创建新组件。继续遍历数据项"two"生成键值"2",存在相同键值,因此不创建新组件。最后遍历数据项"three"时生成键值"3",不存在相同键值,创建内容为"three"的新组件并渲染。
从以上可以看出,当最终键值生成规则包含index时,期望的界面渲染结果为[‘one’, ‘new item’, ‘two’, ‘three’],而实际的渲染结果为[‘one’, ‘two’, ‘three’, ‘three’],渲染结果不符合开发者预期。因此,开发者在使用ForEach时应尽量避免最终键值生成规则中包含index。
2.不推荐案例
在本示例中,ForEach的第三个参数KeyGenerator函数处于缺省状态。根据上述键值生成规则,此例使用框架默认的键值生成规则,即最终键值为字符串index + ‘__’ + JSON.stringify(item)。当点击“在第1项后插入新项”文本组件后,ForEach将需要为第2个数组项以及其后的所有项重新创建组件。
@Entry
@Component
struct Parent {@State simpleList: Array<string> = ['one', 'two', 'three'];build() {Column() {Button() {Text('在第1项后插入新项').fontSize(30)}.onClick(() => {this.simpleList.splice(1, 0, 'new item');console.log(`[onClick]: simpleList is ${JSON.stringify(this.simpleList)}`);})ForEach(this.simpleList, (item: string) => {ChildItem({ item: item })})}.justifyContent(FlexAlign.Center).width('100%').height('100%').backgroundColor(0xF1F3F5)}
}@Component
struct ChildItem {@Prop item: string;aboutToAppear() {console.log(`[aboutToAppear]: item is ${this.item}`);}build() {Text(this.item).fontSize(50)}
}
以上代码的初始渲染效果(左图)和点击"在第1项后插入新项"文本组件后的渲染效果(右图)如下所示。
点击“在第1项后插入新项”文本组件后,IDE的日志打印结果如下所示。
插入新项后,ForEach为new item、 two、 three三个数组项创建了对应的组件ChildItem,并执行了组件的aboutToAppear()生命周期函数。这是因为:
- 在ForEach首次渲染时,创建的键值依次为0__one、1__two、2__three。
- 插入新项后,数据源simpleList变为[‘one’, ‘new item’, ‘two’, ‘three’],ArkUI框架监听到@State装饰的数据源长度变化触发ForEach重新渲染。
- ForEach依次遍历新数据源,遍历数据项one时生成键值0__one,键值已存在,因此不创建新组件。继续遍历数据项new item时生成键值1__new item,不存在相同键值,创建内容为new item的新组件并渲染。继续遍历数据项two生成键值2__two,不存在相同键值,创建内容为two的新组件并渲染。最后遍历数据项three时生成键值3__three,不存在相同键值,创建内容为three的新组件并渲染。
尽管此示例中界面渲染的结果符合预期,但每次插入一条新数组项时,ForEach都会为从该数组项起后面的所有数组项全部重新创建组件。当数据源数据量较大或组件结构复杂时,由于组件无法得到复用,将导致性能体验不佳。因此,除非必要,否则不推荐将第三个参数KeyGenerator函数处于缺省状态,以及在键值生成规则中包含数据项索引index。
数据懒加载(LazyForEach)
LazyForEach和ForEach一样都是接口。LazyForEach从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。当在滚动容器中使用了LazyForEach,框架会根据滚动容器可视区域按需创建组件,当组件滑出可视区域外时,框架会进行组件销毁回收以降低内存占用。
1.接口描述
看一下LazyForEach接口源码
LazyForEach(dataSource: IDataSource, // 需要进行数据迭代的数据源itemGenerator: (item: any, index?: number) => void, // 子组件生成函数keyGenerator?: (item: any, index?: number) => string // 键值生成函数
): void
参数说明:
参数名称 | 参数类型 | 是否必填 | 参数描述 |
---|---|---|---|
dataSource | IDataSource | 是 | LazyForEach数据源,需要开发者实现相关接口。 |
itemGenerator | (item: any, index?:number) => void | 是 | 子组件生成函数,为数组中的每一个数据项创建一个子组件。说明:1: item是当前数据项,index是数据项索引值; 2: itemGenerator的函数体必须使用大括号{…};3: itemGenerator每次迭代只能并且必须生成一个子组件。itemGenerator中可以使用if语句,但是必须保证if语句每个分支都会创建一个相同类型的子组件;4: itemGenerator中不允许使用ForEach和LazyForEach语句。 |
keyGenerator | (item: any, index?:number) => string | 否 | 键值生成函数,用于给数据源中的每一个数据项生成唯一且固定的键值。当数据项在数组中的位置更改时,其键值不得更改,当数组中的数据项被新项替换时,被替换项的键值和新项的键值必须不同。键值生成器的功能是可选的,但是,为了使开发框架能够更好地识别数组更改,提高性能,建议提供。如将数组反向时,如果没有提供键值生成器,则LazyForEach中的所有节点都将重建。注意:item是当前数据项,index是数据项索引值。数据源中的每一个数据项生成的键值不能重复。 |
2.IDataSource类型说明
interface IDataSource {totalCount(): number; // 获得数据总数getData(index: number): Object; // 获取索引值对应的数据registerDataChangeListener(listener: DataChangeListener): void; // 注册数据改变的监听器unregisterDataChangeListener(listener: DataChangeListener): void; // 注销数据改变的监听器
}
参数说明:
接口声明 | 参数类型 | 说明 |
---|---|---|
totalCount(): number | - | 获得数据总数。 |
getData(index: number): any | number | 获取索引值index对应的数据。index:获取数据对应的索引值。 |
registerDataChangeListener(listener:DataChangeListener): void | DataChangeListener | 注册数据改变的监听器。listener:数据变化监听器 |
unregisterDataChangeListener(listener:DataChangeListener): void | DataChangeListener | 注销数据改变的监听器。listener:数据变化监听器 |
3.DataChangeListener类型说明
interface DataChangeListener {onDataReloaded(): void; // 重新加载数据时调用onDataAdded(index: number): void; // 添加数据时调用onDataMoved(from: number, to: number): void; // 数据移动起始位置与数据移动目标位置交换时调用onDataDeleted(index: number): void; // 删除数据时调用onDataChanged(index: number): void; // 改变数据时调用onDataAdd(index: number): void; // 添加数据时调用onDataMove(from: number, to: number): void; // 数据移动起始位置与数据移动目标位置交换时调用onDataDelete(index: number): void; // 删除数据时调用onDataChange(index: number): void; // 改变数据时调用
}
参数说明:
接口声明 | 参数类型 | 说明 |
---|---|---|
onDataReloaded(): void | - | 通知组件重新加载所有数据。键值没有变化的数据项会使用原先的子组件,键值发生变化的会重建子组件。 |
onDataAdd(index: number): void8+ | number | 通知组件index的位置有数据添加。index:数据添加位置的索引值。 |
onDataMove(from: number, to: number): void8+ | from: number;to: number | 通知组件数据有移动。from: 数据移动起始位置,to: 数据移动目标位置。说明:数据移动前后键值要保持不变,如果键值有变化,应使用删除数据和新增数据接口。 |
onDataDelete(index: number):void8+ | number | 通知组件删除index位置的数据并刷新LazyForEach的展示内容。index:数据删除位置的索引值。说明:需要保证dataSource中的对应数据已经在调用onDataDelete前删除,否则页面渲染将出现未定义的行为。 |
onDataChange(index: number): void8+ | number | 通知组件index的位置有数据有变化。index:数据变化位置的索引值。 |
onDataAdded(index: number):void(deprecated) | number | 通知组件index的位置有数据添加。从API 8开始,建议使用onDataAdd。index:数据添加位置的索引值。 |
onDataMoved(from: number, to: number): void(deprecated) | from: number;to: number | 通知组件数据有移动。从API 8开始,建议使用onDataMove。from: 数据移动起始位置,to: 数据移动目标位置。将from和to位置的数据进行交换。说明:数据移动前后键值要保持不变,如果键值有变化,应使用删除数据和新增数据接口。 |
onDataDeleted(index: number):void(deprecated) | number | 通知组件删除index位置的数据并刷新LazyForEach的展示内容。从API 8开始,建议使用onDataDelete。index:数据删除位置的索引值。 |
onDataChanged(index: number): void(deprecated) | number | 通知组件index的位置有数据有变化。从API 8开始,建议使用onDataChange。index:数据变化监听器。 |
4.使用限制
- LazyForEach必须在容器组件内使用,仅有List、Grid、Swiper以及WaterFlow组件支持数据懒加载(可配置cachedCount属性,即只加载可视部分以及其前后少量数据用于缓冲),其他组件仍然是一次性加载所有的数据。
- LazyForEach在每次迭代中,必须创建且只允许创建一个子组件。
- 生成的子组件必须是允许包含在LazyForEach父容器组件中的子组件。
- 允许LazyForEach包含在if/else条件渲染语句中,也允许LazyForEach中出现if/else条件渲染语句。
- 键值生成器必须针对每个数据生成唯一的值,如果键值相同,将导致键值相同的UI组件被框架忽略,从而无法在父容器内显示。
- LazyForEach必须使用DataChangeListener对象来进行更新,第一个参数dataSource使用状态变量时,状态变量改变不会触发LazyForEach的UI刷新。
- 为了高性能渲染,通过DataChangeListener对象的onDataChange方法来更新UI时,需要生成不同于原来的键值来触发组件刷新。
5.键值生成规则
在LazyForEach循环渲染过程中,系统会为每个item生成一个唯一且持久的键值,用于标识对应的组件。当这个键值变化时,ArkUI框架将视为该数组元素已被替换或修改,并会基于新的键值创建一个新的组件。
LazyForEach提供了一个名为keyGenerator的参数,这是一个函数,开发者可以通过它自定义键值的生成规则。如果开发者没有定义keyGenerator函数,则ArkUI框架会使用默认的键值生成函数,即(item: any, index: number) => { return viewId + ‘-’ + index.toString(); }, viewId在编译器转换过程中生成,同一个LazyForEach组件内其viewId是一致的。
6.组件创建规则
在确定键值生成规则后,LazyForEach的第二个参数itemGenerator函数会根据键值生成规则为数据源的每个数组项创建组件。组件的创建包括两种情况:LazyForEach首次渲染和LazyForEach非首次渲染。
1.首次渲染
- 生成不同键值
在LazyForEach首次渲染时,会根据上述键值生成规则为数据源的每个数组项生成唯一键值,并创建相应的组件。
// Basic implementation of IDataSource to handle data listener
class BasicDataSource implements IDataSource {private listeners: DataChangeListener[] = [];private originDataArray: string[] = [];public totalCount(): number {return 0;}public getData(index: number): string {return this.originDataArray[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);})}
}class MyDataSource extends BasicDataSource {private dataArray: string[] = [];public totalCount(): number {return this.dataArray.length;}public getData(index: number): string {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();aboutToAppear() {for (let i = 0; i <= 20; i++) {this.data.pushData(`Hello ${i}`)}}build() {List({ space: 3 }) {LazyForEach(this.data, (item: string) => {ListItem() {Row() {Text(item).fontSize(50).onAppear(() => {console.info("appear:" + item)})}.margin({ left: 10, right: 10 })}}, (item: string) => item)}.cachedCount(5)}
}
在上述代码中,键值生成规则是keyGenerator函数的返回值item。在LazyForEach循环渲染时,其为数据源数组项依次生成键值Hello 0、Hello 1 … Hello 20,并创建对应的ListItem子组件渲染到界面上。
运行效果如下图所示:
- 键值相同时错误渲染
当不同数据项生成的键值相同时,框架的行为是不可预测的。例如,在以下代码中,LazyForEach渲染的数据项键值均相同,在滑动过程中,LazyForEach会对划入划出当前页面的子组件进行预加载,而新建的子组件和销毁的原子组件具有相同的键值,框架可能存在取用缓存错误的情况,导致子组件渲染有问题。
class BasicDataSource implements IDataSource {private listeners: DataChangeListener[] = [];private originDataArray: string[] = [];public totalCount(): number {return 0;}public getData(index: number): string {return this.originDataArray[index];}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);})}
}class MyDataSource extends BasicDataSource {private dataArray: string[] = [];public totalCount(): number {return this.dataArray.length;}public getData(index: number): string {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();aboutToAppear() {for (let i = 0; i <= 20; i++) {this.data.pushData(`Hello ${i}`)}}build() {List({ space: 3 }) {LazyForEach(this.data, (item: string) => {ListItem() {Row() {Text(item).fontSize(50).onAppear(() => {console.info("appear:" + item)})}.margin({ left: 10, right: 10 })}}, (item: string) => 'same key')}.cachedCount(5)}
}
运行效果如下图所示。可以看到Hello 0在滑动过程中被错误渲染为Hello 13。
2.非首次渲染
当LazyForEach数据源发生变化,需要再次渲染时,开发者应根据数据源的变化情况调用listener对应的接口,通知LazyForEach做相应的更新,各使用场景如下。
- 添加数据
class BasicDataSource implements IDataSource {private listeners: DataChangeListener[] = [];private originDataArray: string[] = [];public totalCount(): number {return 0;}public getData(index: number): string {return this.originDataArray[index];}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);})}
}class MyDataSource extends BasicDataSource {private dataArray: string[] = [];public totalCount(): number {return this.dataArray.length;}public getData(index: number): string {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();aboutToAppear() {for (let i = 0; i <= 20; i++) {this.data.pushData(`Hello ${i}`)}}build() {List({ space: 3 }) {LazyForEach(this.data, (item: string) => {ListItem() {Row() {Text(item).fontSize(50).onAppear(() => {console.info("appear:" + item)})}.margin({ left: 10, right: 10 })}.onClick(() => {// 点击追加子组件this.data.pushData(`Hello ${this.data.totalCount()}`);})}, (item: string) => item)}.cachedCount(5)}
}
当我们点击LazyForEach的子组件时,首先调用数据源data的pushData方法,该方法会在数据源末尾添加数据并调用notifyDataAdd方法。在notifyDataAdd方法内会又调用listener.onDataAdd方法,该方法会通知LazyForEach在该处有数据添加,LazyForEach便会在该索引处新建子组件。
运行效果如下图所示。
- 删除数据
class BasicDataSource implements IDataSource {private listeners: DataChangeListener[] = [];private originDataArray: string[] = [];public totalCount(): number {return 0;}public getData(index: number): string {return this.originDataArray[index];}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);})}
}class MyDataSource extends BasicDataSource {dataArray: string[] = [];public totalCount(): number {return this.dataArray.length;}public getData(index: number): string {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);}public deleteData(index: number): void {this.dataArray.splice(index, 1);this.notifyDataDelete(index);}
}@Entry
@Component
struct MyComponent {private data: MyDataSource = new MyDataSource();aboutToAppear() {for (let i = 0; i <= 20; i++) {this.data.pushData(`Hello ${i}`)}}build() {List({ space: 3 }) {LazyForEach(this.data, (item: string, index: number) => {ListItem() {Row() {Text(item).fontSize(50).onAppear(() => {console.info("appear:" + item)})}.margin({ left: 10, right: 10 })}.onClick(() => {// 点击删除子组件this.data.deleteData(this.data.dataArray.indexOf(item));})}, (item: string) => item)}.cachedCount(5)}
}
当我们点击LazyForEach的子组件时,首先调用数据源data的deleteData方法,该方法会删除数据源对应索引处的数据并调用notifyDatDelete方法。在notifyDataDelete方法内会又调用listener.onDataDelete方法,该方法会通知LazyForEach在该处有数据删除,LazyForEach便会在该索引处删除对应子组件。
运行效果如下图所示。
- 改变单个数据
class BasicDataSource implements IDataSource {private listeners: DataChangeListener[] = [];private originDataArray: string[] = [];public totalCount(): number {return 0;}public getData(index: number): string {return this.originDataArray[index];}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);})}
}class MyDataSource extends BasicDataSource {private dataArray: string[] = [];public totalCount(): number {return this.dataArray.length;}public getData(index: number): string {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);}public deleteData(index: number): void {this.dataArray.splice(index, 1);this.notifyDataDelete(index);}public changeData(index: number, data: string): void {this.dataArray.splice(index, 1, data);this.notifyDataChange(index);}
}@Entry
@Component
struct MyComponent {private moved: number[] = [];private data: MyDataSource = new MyDataSource();aboutToAppear() {for (let i = 0; i <= 20; i++) {this.data.pushData(`Hello ${i}`)}}build() {List({ space: 3 }) {LazyForEach(this.data, (item: string, index: number) => {ListItem() {Row() {Text(item).fontSize(50).onAppear(() => {console.info("appear:" + item)})}.margin({ left: 10, right: 10 })}.onClick(() => {this.data.changeData(index, item + '00');})}, (item: string) => item)}.cachedCount(5)}
}
当我们点击LazyForEach的子组件时,首先改变当前数据,然后调用数据源data的changeData方法,在该方法内会调用notifyDataChange方法。在notifyDataChange方法内会又调用listener.onDataChange方法,该方法通知LazyForEach组件该处有数据发生变化,LazyForEach便会在对应索引处重建子组件。
运行效果如下图所示。
- 改变多个数据
class BasicDataSource implements IDataSource {private listeners: DataChangeListener[] = [];private originDataArray: string[] = [];public totalCount(): number {return 0;}public getData(index: number): string {return this.originDataArray[index];}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);})}
}class MyDataSource extends BasicDataSource {private dataArray: string[] = [];public totalCount(): number {return this.dataArray.length;}public getData(index: number): string {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);}public deleteData(index: number): void {this.dataArray.splice(index, 1);this.notifyDataDelete(index);}public changeData(index: number): void {this.notifyDataChange(index);}public reloadData(): void {this.notifyDataReload();}public modifyAllData(): void {this.dataArray = this.dataArray.map((item: string) => {return item + '0';})}
}@Entry
@Component
struct MyComponent {private moved: number[] = [];private data: MyDataSource = new MyDataSource();aboutToAppear() {for (let i = 0; i <= 20; i++) {this.data.pushData(`Hello ${i}`)}}build() {List({ space: 3 }) {LazyForEach(this.data, (item: string, index: number) => {ListItem() {Row() {Text(item).fontSize(50).onAppear(() => {console.info("appear:" + item)})}.margin({ left: 10, right: 10 })}.onClick(() => {this.data.modifyAllData();this.data.reloadData();})}, (item: string) => item)}.cachedCount(5)}
}
当我们点击LazyForEach的子组件时,首先调用data的modifyAllData方法改变了数据源中的所有数据,然后调用数据源的reloadData方法,在该方法内会调用notifyDataReload方法。在notifyDataReload方法内会又调用listener.onDataReloaded方法,通知LazyForEach需要重建所有子节点。LazyForEach会将原所有数据项和新所有数据项一一做键值比对,若有相同键值则使用缓存,若键值不同则重新构建。
运行效果如下图所示。
- 改变数据子属性
若仅靠LazyForEach的刷新机制,当item变化时若想更新子组件,需要将原来的子组件全部销毁再重新构建,在子组件结构较为复杂的情况下,靠改变键值去刷新渲染性能较低。因此框架提供了@Observed与@ObjectLink机制进行深度观测,可以做到仅刷新使用了该属性的组件,提高渲染性能。开发者可根据其自身业务特点选择使用哪种刷新方式。
class BasicDataSource implements IDataSource {private listeners: DataChangeListener[] = [];private originDataArray: StringData[] = [];public totalCount(): number {return 0;}public getData(index: number): StringData {return this.originDataArray[index];}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);})}
}class MyDataSource extends BasicDataSource {private dataArray: StringData[] = [];public totalCount(): number {return this.dataArray.length;}public getData(index: number): StringData {return this.dataArray[index];}public addData(index: number, data: StringData): void {this.dataArray.splice(index, 0, data);this.notifyDataAdd(index);}public pushData(data: StringData): void {this.dataArray.push(data);this.notifyDataAdd(this.dataArray.length - 1);}
}@Observed
class StringData {message: string;constructor(message: string) {this.message = message;}
}@Entry
@Component
struct MyComponent {private moved: number[] = [];@State data: MyDataSource = new MyDataSource();aboutToAppear() {for (let i = 0; i <= 20; i++) {this.data.pushData(new StringData(`Hello ${i}`));}}build() {List({ space: 3 }) {LazyForEach(this.data, (item: StringData, index: number) => {ListItem() {ChildComponent({data: item})}.onClick(() => {item.message += '0';})}, (item: StringData, index: number) => index.toString())}.cachedCount(5)}
}@Component
struct ChildComponent {@ObjectLink data: StringDatabuild() {Row() {Text(this.data.message).fontSize(50).onAppear(() => {console.info("appear:" + this.data.message)})}.margin({ left: 10, right: 10 })}
}
此时点击LazyForEach子组件改变item.message时,重渲染依赖的是ChildComponent的@ObjectLink成员变量对其子属性的监听,此时框架只会刷新Text(this.data.message),不会去重建整个ListItem子组件。
7.常见使用问题
1.渲染结果非预期
class BasicDataSource implements IDataSource {private listeners: DataChangeListener[] = [];private originDataArray: string[] = [];public totalCount(): number {return 0;}public getData(index: number): string {return this.originDataArray[index];}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);})}
}class MyDataSource extends BasicDataSource {private dataArray: string[] = [];public totalCount(): number {return this.dataArray.length;}public getData(index: number): string {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);}public deleteData(index: number): void {this.dataArray.splice(index, 1);this.notifyDataDelete(index);}
}@Entry
@Component
struct MyComponent {private data: MyDataSource = new MyDataSource();aboutToAppear() {for (let i = 0; i <= 20; i++) {this.data.pushData(`Hello ${i}`)}}build() {List({ space: 3 }) {LazyForEach(this.data, (item: string, index: number) => {ListItem() {Row() {Text(item).fontSize(50).onAppear(() => {console.info("appear:" + item)})}.margin({ left: 10, right: 10 })}.onClick(() => {// 点击删除子组件this.data.deleteData(index);})}, (item: string) => item)}.cachedCount(5)}
}
当我们多次点击子组件时,会发现删除的并不一定是我们点击的那个子组件。原因是当我们删除了某一个子组件后,位于该子组件对应的数据项之后的各数据项,其index均应减1,但实际上后续的数据项对应的子组件仍然使用的是最初分配的index,其itemGenerator中的index并没有发生变化,所以删除结果和预期不符。
修复代码如下所示。
class BasicDataSource implements IDataSource {private listeners: DataChangeListener[] = [];private originDataArray: string[] = [];public totalCount(): number {return 0;}public getData(index: number): string {return this.originDataArray[index];}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);})}
}class MyDataSource extends BasicDataSource {private dataArray: string[] = [];public totalCount(): number {return this.dataArray.length;}public getData(index: number): string {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);}public deleteData(index: number): void {this.dataArray.splice(index, 1);this.notifyDataDelete(index);}public reloadData(): void {this.notifyDataReload();}
}@Entry
@Component
struct MyComponent {private data: MyDataSource = new MyDataSource();aboutToAppear() {for (let i = 0; i <= 20; i++) {this.data.pushData(`Hello ${i}`)}}build() {List({ space: 3 }) {LazyForEach(this.data, (item: string, index: number) => {ListItem() {Row() {Text(item).fontSize(50).onAppear(() => {console.info("appear:" + item)})}.margin({ left: 10, right: 10 })}.onClick(() => {// 点击删除子组件this.data.deleteData(index);// 重置所有子组件的index索引this.data.reloadData();})}, (item: string, index: number) => item + index.toString())}.cachedCount(5)}
}
在删除一个数据项后调用reloadData方法,重建后面的数据项,以达到更新index索引的目的。
2:重渲染时图片闪烁
class BasicDataSource implements IDataSource {private listeners: DataChangeListener[] = [];private originDataArray: StringData[] = [];public totalCount(): number {return 0;}public getData(index: number): StringData {return this.originDataArray[index];}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);})}
}class MyDataSource extends BasicDataSource {private dataArray: StringData[] = [];public totalCount(): number {return this.dataArray.length;}public getData(index: number): StringData {return this.dataArray[index];}public addData(index: number, data: StringData): void {this.dataArray.splice(index, 0, data);this.notifyDataAdd(index);}public pushData(data: StringData): void {this.dataArray.push(data);this.notifyDataAdd(this.dataArray.length - 1);}public reloadData(): void {this.notifyDataReload();}
}class StringData {message: string;imgSrc: Resource;constructor(message: string, imgSrc: Resource) {this.message = message;this.imgSrc = imgSrc;}
}@Entry
@Component
struct MyComponent {private moved: number[] = [];private data: MyDataSource = new MyDataSource();aboutToAppear() {for (let i = 0; i <= 20; i++) {this.data.pushData(new StringData(`Hello ${i}`, $r('app.media.img')));}}build() {List({ space: 3 }) {LazyForEach(this.data, (item: StringData, index: number) => {ListItem() {Column() {Text(item.message).fontSize(50).onAppear(() => {console.info("appear:" + item.message)})Image(item.imgSrc).width(500).height(200)}.margin({ left: 10, right: 10 })}.onClick(() => {item.message += '00';this.data.reloadData();})}, (item: StringData, index: number) => JSON.stringify(item))}.cachedCount(5)}
}
LazyForEach仅改变文字但是图片闪烁问题。
在我们点击ListItem子组件时,我们只改变了数据项的message属性,但是LazyForEach的刷新机制会导致整个ListItem被重建。由于Image组件是异步刷新,所以视觉上图片会发生闪烁。为了解决这种情况我们应该使用@ObjectLink和@Observed去单独刷新使用了item.message的Text组件。
修复代码如下所示。
class BasicDataSource implements IDataSource {private listeners: DataChangeListener[] = [];private originDataArray: StringData[] = [];public totalCount(): number {return 0;}public getData(index: number): StringData {return this.originDataArray[index];}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);})}
}class MyDataSource extends BasicDataSource {private dataArray: StringData[] = [];public totalCount(): number {return this.dataArray.length;}public getData(index: number): StringData {return this.dataArray[index];}public addData(index: number, data: StringData): void {this.dataArray.splice(index, 0, data);this.notifyDataAdd(index);}public pushData(data: StringData): void {this.dataArray.push(data);this.notifyDataAdd(this.dataArray.length - 1);}
}@Observed
class StringData {message: string;imgSrc: Resource;constructor(message: string, imgSrc: Resource) {this.message = message;this.imgSrc = imgSrc;}
}@Entry
@Component
struct MyComponent {@State data: MyDataSource = new MyDataSource();aboutToAppear() {for (let i = 0; i <= 20; i++) {this.data.pushData(new StringData(`Hello ${i}`, $r('app.media.img')));}}build() {List({ space: 3 }) {LazyForEach(this.data, (item: StringData, index: number) => {ListItem() {ChildComponent({data: item})}.onClick(() => {item.message += '0';})}, (item: StringData, index: number) => index.toString())}.cachedCount(5)}
}@Component
struct ChildComponent {@ObjectLink data: StringDatabuild() {Column() {Text(this.data.message).fontSize(50).onAppear(() => {console.info("appear:" + this.data.message)})Image(this.data.imgSrc).width(500).height(200)}.margin({ left: 10, right: 10 })}
}
修复LazyForEach仅改变文字但是图片闪烁问题
3.@ObjectLink属性变化UI未更新
class BasicDataSource implements IDataSource {private listeners: DataChangeListener[] = [];private originDataArray: StringData[] = [];public totalCount(): number {return 0;}public getData(index: number): StringData {return this.originDataArray[index];}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);})}
}class MyDataSource extends BasicDataSource {private dataArray: StringData[] = [];public totalCount(): number {return this.dataArray.length;}public getData(index: number): StringData {return this.dataArray[index];}public addData(index: number, data: StringData): void {this.dataArray.splice(index, 0, data);this.notifyDataAdd(index);}public pushData(data: StringData): void {this.dataArray.push(data);this.notifyDataAdd(this.dataArray.length - 1);}
}@Observed
class StringData {message: NestedString;constructor(message: NestedString) {this.message = message;}
}@Observed
class NestedString {message: string;constructor(message: string) {this.message = message;}
}@Entry
@Component
struct MyComponent {private moved: number[] = [];@State data: MyDataSource = new MyDataSource();aboutToAppear() {for (let i = 0; i <= 20; i++) {this.data.pushData(new StringData(new NestedString(`Hello ${i}`)));}}build() {List({ space: 3 }) {LazyForEach(this.data, (item: StringData, index: number) => {ListItem() {ChildComponent({data: item})}.onClick(() => {item.message.message += '0';})}, (item: StringData, index: number) => item.toString() + index.toString())}.cachedCount(5)}
}@Component
struct ChildComponent {@ObjectLink data: StringDatabuild() {Row() {Text(this.data.message.message).fontSize(50).onAppear(() => {console.info("appear:" + this.data.message.message)})}.margin({ left: 10, right: 10 })}
}
ObjectLink属性变化后UI未更新
@ObjectLink装饰的成员变量仅能监听到其子属性的变化,再深入嵌套的属性便无法观测到了,因此我们只能改变它的子属性去通知对应组件重新渲染,具体请查看@ObjectLink与@Observed的详细使用方法和限制条件。
修复代码如下所示。
class BasicDataSource implements IDataSource {private listeners: DataChangeListener[] = [];private originDataArray: StringData[] = [];public totalCount(): number {return 0;}public getData(index: number): StringData {return this.originDataArray[index];}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);})}
}class MyDataSource extends BasicDataSource {private dataArray: StringData[] = [];public totalCount(): number {return this.dataArray.length;}public getData(index: number): StringData {return this.dataArray[index];}public addData(index: number, data: StringData): void {this.dataArray.splice(index, 0, data);this.notifyDataAdd(index);}public pushData(data: StringData): void {this.dataArray.push(data);this.notifyDataAdd(this.dataArray.length - 1);}
}@Observed
class StringData {message: NestedString;constructor(message: NestedString) {this.message = message;}
}@Observed
class NestedString {message: string;constructor(message: string) {this.message = message;}
}@Entry
@Component
struct MyComponent {private moved: number[] = [];@State data: MyDataSource = new MyDataSource();aboutToAppear() {for (let i = 0; i <= 20; i++) {this.data.pushData(new StringData(new NestedString(`Hello ${i}`)));}}build() {List({ space: 3 }) {LazyForEach(this.data, (item: StringData, index: number) => {ListItem() {ChildComponent({data: item})}.onClick(() => {item.message = new NestedString(item.message.message + '0');})}, (item: StringData, index: number) => item.toString() + index.toString())}.cachedCount(5)}
}@Component
struct ChildComponent {@ObjectLink data: StringDatabuild() {Row() {Text(this.data.message.message).fontSize(50).onAppear(() => {console.info("appear:" + this.data.message.message)})}.margin({ left: 10, right: 10 })}
}
修复ObjectLink属性变化后UI更新
总结
ArkUI通过自定义组件的build()函数和@builder装饰器中的声明式UI描述语句构建相应的UI。在声明式描述语句中开发者除了使用系统组件外,还可以使用渲染控制语句来辅助UI的构建,这些渲染控制语句包括控制组件是否显示的条件渲染语句,基于数组数据快速生成组件的循环渲染语句以及针对大数据量场景的数据懒加载语句。