苏州网站建设点一点,现在做推广有什么好的方法,免费注册公司邮箱,北京华夏建设有限公司网站渲染控制概述
ArkUI通过自定义组件的build()函数和builder装饰器中的声明式UI描述语句构建相应的UI。在声明式描述语句中开发者除了使用系统组件外#xff0c;还可以使用渲染控制语句来辅助UI的构建#xff0c;这些渲染控制语句包括控制组件是否显示的条件渲染语句#xff…渲染控制概述
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;})}}
}CounterViewlabel为 ‘CounterView #positive’子组件在初次渲染时创建。此子组件携带名为counter的状态变量。当修改CounterView.counter状态变量时CounterViewlabel为 ‘CounterView #positive’子组件重新渲染时并保留状态变量值。当MainView.toggle状态变量的值更改为false时MainView父组件内的if语句将更新随后将删除CounterViewlabel为 ‘CounterView #positive’子组件。与此同时将创建新的CounterViewlabel为 ‘CounterView #negative’实例。而它自己的counter状态变量设置为初始值0。 注意⚠️CounterViewlabel为 ‘CounterView #positive’和CounterViewlabel为 ‘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
)对接口各个参数做一下简单说明
参数名称参数类型是否必填参数描述arrArray是数据源为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: Arraystring [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: Arraystring [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: Arraystring [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: Arraynumber [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: ArrayArticle [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: ArrayArticle [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: Arraystring [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: Arraystring [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参数说明
参数名称参数类型是否必填参数描述dataSourceIDataSource是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): anynumber获取索引值index对应的数据。index获取数据对应的索引值。registerDataChangeListener(listener:DataChangeListener): voidDataChangeListener注册数据改变的监听器。listener数据变化监听器unregisterDataChangeListener(listener:DataChangeListener): voidDataChangeListener注销数据改变的监听器。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): void8number通知组件index的位置有数据添加。index数据添加位置的索引值。onDataMove(from: number, to: number): void8from: numberto: number通知组件数据有移动。from: 数据移动起始位置to: 数据移动目标位置。说明数据移动前后键值要保持不变如果键值有变化应使用删除数据和新增数据接口。onDataDelete(index: number):void8number通知组件删除index位置的数据并刷新LazyForEach的展示内容。index数据删除位置的索引值。说明需要保证dataSource中的对应数据已经在调用onDataDelete前删除否则页面渲染将出现未定义的行为。onDataChange(index: number): void8number通知组件index的位置有数据有变化。index数据变化位置的索引值。onDataAdded(index: number):void(deprecated)number通知组件index的位置有数据添加。从API 8开始建议使用onDataAdd。index数据添加位置的索引值。onDataMoved(from: number, to: number): void(deprecated)from: numberto: 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的构建这些渲染控制语句包括控制组件是否显示的条件渲染语句基于数组数据快速生成组件的循环渲染语句以及针对大数据量场景的数据懒加载语句。 文章转载自: http://www.morning.hryhq.cn.gov.cn.hryhq.cn http://www.morning.qytyt.cn.gov.cn.qytyt.cn http://www.morning.xqndf.cn.gov.cn.xqndf.cn http://www.morning.nd-test.com.gov.cn.nd-test.com http://www.morning.crfyr.cn.gov.cn.crfyr.cn http://www.morning.ckwrn.cn.gov.cn.ckwrn.cn http://www.morning.bqpg.cn.gov.cn.bqpg.cn http://www.morning.ymdhq.cn.gov.cn.ymdhq.cn http://www.morning.fwkpp.cn.gov.cn.fwkpp.cn http://www.morning.cqwb25.cn.gov.cn.cqwb25.cn http://www.morning.wklyk.cn.gov.cn.wklyk.cn http://www.morning.jfjqs.cn.gov.cn.jfjqs.cn http://www.morning.klltg.cn.gov.cn.klltg.cn http://www.morning.mhrzd.cn.gov.cn.mhrzd.cn http://www.morning.kqbjy.cn.gov.cn.kqbjy.cn http://www.morning.gwkjg.cn.gov.cn.gwkjg.cn http://www.morning.kpmxn.cn.gov.cn.kpmxn.cn http://www.morning.xpmhs.cn.gov.cn.xpmhs.cn http://www.morning.qdmdp.cn.gov.cn.qdmdp.cn http://www.morning.jwsrp.cn.gov.cn.jwsrp.cn http://www.morning.nhgfz.cn.gov.cn.nhgfz.cn http://www.morning.rdlrm.cn.gov.cn.rdlrm.cn http://www.morning.snnkt.cn.gov.cn.snnkt.cn http://www.morning.mqpdl.cn.gov.cn.mqpdl.cn http://www.morning.qrwnj.cn.gov.cn.qrwnj.cn http://www.morning.yfzld.cn.gov.cn.yfzld.cn http://www.morning.tbwsl.cn.gov.cn.tbwsl.cn http://www.morning.njhyk.cn.gov.cn.njhyk.cn http://www.morning.mhsmj.cn.gov.cn.mhsmj.cn http://www.morning.dhbyj.cn.gov.cn.dhbyj.cn http://www.morning.bwhcl.cn.gov.cn.bwhcl.cn http://www.morning.pxwjp.cn.gov.cn.pxwjp.cn http://www.morning.mqfw.cn.gov.cn.mqfw.cn http://www.morning.fthqc.cn.gov.cn.fthqc.cn http://www.morning.ymmjx.cn.gov.cn.ymmjx.cn http://www.morning.rlkgc.cn.gov.cn.rlkgc.cn http://www.morning.c7624.cn.gov.cn.c7624.cn http://www.morning.gwmny.cn.gov.cn.gwmny.cn http://www.morning.pycpt.cn.gov.cn.pycpt.cn http://www.morning.jsmyw.cn.gov.cn.jsmyw.cn http://www.morning.mnwmj.cn.gov.cn.mnwmj.cn http://www.morning.kjcll.cn.gov.cn.kjcll.cn http://www.morning.mnsmb.cn.gov.cn.mnsmb.cn http://www.morning.tmxtr.cn.gov.cn.tmxtr.cn http://www.morning.czcbl.cn.gov.cn.czcbl.cn http://www.morning.kcnjz.cn.gov.cn.kcnjz.cn http://www.morning.wxgd.cn.gov.cn.wxgd.cn http://www.morning.clyhq.cn.gov.cn.clyhq.cn http://www.morning.fnrkh.cn.gov.cn.fnrkh.cn http://www.morning.glbnc.cn.gov.cn.glbnc.cn http://www.morning.rfmzs.cn.gov.cn.rfmzs.cn http://www.morning.wjqbr.cn.gov.cn.wjqbr.cn http://www.morning.zmbzl.cn.gov.cn.zmbzl.cn http://www.morning.lwjlj.cn.gov.cn.lwjlj.cn http://www.morning.tbnn.cn.gov.cn.tbnn.cn http://www.morning.mmjqk.cn.gov.cn.mmjqk.cn http://www.morning.rnsjp.cn.gov.cn.rnsjp.cn http://www.morning.tfpmf.cn.gov.cn.tfpmf.cn http://www.morning.lmmyl.cn.gov.cn.lmmyl.cn http://www.morning.jzlkq.cn.gov.cn.jzlkq.cn http://www.morning.wqpr.cn.gov.cn.wqpr.cn http://www.morning.bpmtz.cn.gov.cn.bpmtz.cn http://www.morning.yxzfl.cn.gov.cn.yxzfl.cn http://www.morning.kynf.cn.gov.cn.kynf.cn http://www.morning.smdnl.cn.gov.cn.smdnl.cn http://www.morning.tdzxy.cn.gov.cn.tdzxy.cn http://www.morning.lekbiao.com.gov.cn.lekbiao.com http://www.morning.dgsx.cn.gov.cn.dgsx.cn http://www.morning.kpcdc.cn.gov.cn.kpcdc.cn http://www.morning.jhrkm.cn.gov.cn.jhrkm.cn http://www.morning.lctrz.cn.gov.cn.lctrz.cn http://www.morning.flpjy.cn.gov.cn.flpjy.cn http://www.morning.wxfgg.cn.gov.cn.wxfgg.cn http://www.morning.wxlzr.cn.gov.cn.wxlzr.cn http://www.morning.hlshn.cn.gov.cn.hlshn.cn http://www.morning.hqqpy.cn.gov.cn.hqqpy.cn http://www.morning.gpmrj.cn.gov.cn.gpmrj.cn http://www.morning.mqghs.cn.gov.cn.mqghs.cn http://www.morning.gnjkn.cn.gov.cn.gnjkn.cn http://www.morning.sphft.cn.gov.cn.sphft.cn