案例解析关于ArkUI框架中ForEach的潜在陷阱与性能优化

本文分享自华为云社区《深入解析ForEach的潜在陷阱与性能优化:错误用法与性能下降的案例分析》,作者:柠檬味拥抱 。

在ArkUI框架中,ForEach接口是基于数组类型数据进行循环渲染的强大工具。它需要与容器组件搭配使用,并能够根据数据源动态生成相应的子组件。以下是对ForEach接口的详细解析,包括接口描述、参数说明、键值生成规则以及使用场景的示例。

ForEach接口概述

介绍

ForEach接口基于数组类型数据进行循环渲染,需要与容器组件配合使用。它能够根据数据源的变化,动态生成对应的子组件,并将其渲染到界面上。

接口描述

typescript 复制代码
ForEach(

arr: Array,

itemGenerator: (item: Array, index?: number) => void,

keyGenerator?: (item: Array, index?: number) => string

)

参数说明

  • arr: 数据源,为Array类型的数组。可以为空数组,但不能改变包括数组本身在内的任何状态变量。
  • itemGenerator: 组件生成函数,为数组中的每个元素创建对应的组件。必须是ForEach父容器所允许的子组件类型。
  • keyGenerator: 键值生成函数,为数据源的每个数组项生成唯一且持久的键值。可选参数,如果未定义,将使用默认的键值生成函数。

键值生成规则

在ForEach循环渲染过程中,系统会为每个数组元素生成一个唯一且持久的键值,用于标识对应的组件。键值的生成规则由开发者定义,可以使用默认规则或自定义规则。

默认规则

perl 复制代码
(item: T, index: number) => { return index + '__' + JSON.stringify(item); }

系统对于键值的生成判断主要与itemGenerator函数的第二个参数index以及keyGenerator函数的返回值有关。具体的键值生成规则判断逻辑可以参考文档中的图1 ForEach键值生成规则。

组件创建规则

在确定键值生成规则后,ForEach的itemGenerator函数会根据键值生成规则为数据源的每个数组项创建组件。组件的创建包括两种情况:ForEach首次渲染和ForEach非首次渲染。

首次渲染

在首次渲染时,根据键值生成规则为数据源的每个数组项生成唯一键值,并创建相应的组件。以下是一个示例代码:

scss 复制代码
@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)

}

}

在上述代码中,根据键值生成规则为数据源simpleList的每个数组项生成唯一键值,并创建对应的ChildItem组件渲染到界面上。

非首次渲染

在非首次渲染时,ForEach会检查新生成的键值是否在上次渲染中已经存在。如果键值不存在,则会创建一个新的组件;如果键值存在,则不会创建新的组件,而是直接渲染该键值所对应的组件。以下是一个示例代码:

scss 复制代码
@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)

}

}

在上述代码中,当渲染到索引为2的'two'时,因为该键值已存在于上次渲染中,所以不会创建新的ChildItem组件,而是直接复用已有的组件。

使用场景

ForEach组件在开发过程中的主要应用场景包括:

  1. 数据源不变: 数据源可以直接采用基本数据类型数组,在页面加载状态时使用骨架屏列表进行渲染展示。
  2. 数据源数组项发生变化: 在数据源数组项发生变化的场景下,例如进行数组插入、删除操作或者数组项索引位置发生交换时,数据源应为对象数组类型,并使用对象的唯一ID作为最终键值。
  3. 数据源数组项子属性变化: 当数据源的数组项为对象数据类型,并且只修改某个数组项的属性值时,需要结合@Observed和@ObjectLink装饰器使用,以实现ForEach的重新渲染。

以下是一个示例代码,演示了上述场景的应用:

arduino 复制代码
// 示例代码略,可根据文档中的使用场景示例进行参考

在该示例中,通过对Article类使用@Observed和@ObjectLink装饰器,实现了数据源数组项子属性变化时的重新渲染。当点击文章卡片上的点赞按钮时,修改了数组项的点赞状态和点赞数量,触发了ForEach的重新渲染。

高级用法

条件渲染逻辑

ForEach的itemGenerator函数支持包含if/else条件渲染逻辑。这意味着你可以根据数据源的不同条件,动态生成不同类型的子组件。以下是一个示例代码:

scss 复制代码
@Entry

@Component

struct Parent {

@State condition: boolean = true;

@State dataList: Array<string> = ['item1', 'item2', 'item3'];

build() {

Column() {

ForEach(this.dataList, (item: string, index: number) => {

if (this.condition) {

// 根据条件渲染不同类型的子组件

ChildItem1({ item: item, index: index });

} else {

ChildItem2({ item: item, index: index });

}

}, (item: string) => item);

}

}

}

@Component

struct ChildItem1 {

@Prop item: string;

@Prop index: number;

build() {

Text(`Item ${this.index + 1}: ${this.item}`)

.fontSize(18)

.fontColor(Color.Blue);

}

}

@Component

struct ChildItem2 {

@Prop item: string;

@Prop index: number;

build() {

Text(`Item ${this.index + 1}: ${this.item}`)

.fontSize(16)

.fontColor(Color.Green);

}

}

在上述示例中,根据condition的值,动态选择渲染ChildItem1或ChildItem2。这为根据业务逻辑灵活地调整渲染行为提供了便利。

LazyForEach的性能优化

如果数据源非常庞大或者有特定的性能需求,推荐使用LazyForEach组件进行懒加载。LazyForEach在初始化渲染时不会一次性加载所有数据,而是根据滚动位置和用户操作,动态地加载可见区域的数据项。这有助于提高页面的渲染性能。以下是一个简单的示例代码:

scss 复制代码
@Entry

@Component

struct LazyForEachExample {

@State dataList: Array<string> = [...]; // 大型数据源

build() {

LazyForEach(this.dataList, (item: string, index: number) => {

LazyChildItem({ item: item, index: index });

}, (item: string) => item);

}

}

@Component

struct LazyChildItem {

@Prop item: string;

@Prop index: number;

build() {

Text(`Item ${this.index + 1}: ${this.item}`)

.fontSize(18)

.fontColor(Color.Blue);

}

}

在上述示例中,LazyForEach会根据用户的滚动位置,只加载可见区域的数据项,从而在处理大型数据源时提升性能。

渲染结果预期

在使用ForEach接口时,需要注意渲染结果的预期。在UI更新的场景下,如果出现重复的键值,框架可能无法正常工作。因此,在使用自定义键值生成函数时,需要确保生成的键值具有唯一性,避免出现渲染结果不符合预期的情况。

ForEach的错误使用案例与性能降低

在使用 ForEach 过程中,对于键值生成规则的理解不够充分可能导致错误的使用方式,带来功能和性能方面的问题。以下是一个示例,展示了错误使用 ForEach 可能导致的渲染结果非预期和性能降低的情况。

渲染结果非预期

在下面的示例中,通过设置 ForEach 的第三个参数 KeyGenerator 函数,自定义键值生成规则为数据源的索引 index 的字符串类型值。当点击父组件 Parent 中的"在第1项后插入新项"文本组件后,界面会出现非预期的结果。

scss 复制代码
@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项后插入新项"后的渲染效果如下图所示:

初始渲染时,创建的键值依次为 "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。

渲染性能降低

在下面的示例中,ForEach 的第三个参数 KeyGenerator 函数处于缺省状态。根据上述键值生成规则,此例使用框架默认的键值生成规则,即最终键值为字符串 index + '__' + JSON.stringify(item)。当点击"在第1项后插入新项"文本组件后,ForEach 将需要为第2个数组项以及其后的所有项重新创建组件。

scss 复制代码
@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() 生命周期函数。这是因为:

  1. 在 ForEach 首次渲染时,创建的键值依次为 0__one、1__two、2__three。
  2. 插入新项后,数据源 simpleList 变为 'one', 'new item', 'two', 'three',ArkUI 框架监听到 @State 装饰的数据源长度变化触发 ForEach 重新渲染。
  3. ForEach 依次遍历新数据源,遍历数据项 "one" 时生成键值 0__one,键值已存在,因此不创建新组件。继续遍历数据项 "new item" 时生成键值 1__new item,不存在

相同键值,创建内容为 "new item" 的新组件并渲染。继续遍历数据项 "two" 生成键值 2__two,不存在相同键值,创建内容为 "two" 的新组件并渲染。最后遍历数据项 "three" 时生成键值 3__three,不存在相同键值,创建内容为 "three" 的新组件并渲染。

尽管此示例中界面渲染的结果符合预期,但每次插入一条新数组项时,ForEach 都会为从该数组项起后面的所有数组项全部重新创建组件。当数据源数据量较大或组件结构复杂时,由于组件无法得到复用,将导致性能体验不佳。因此,除非必要,否则不推荐将第三个参数 KeyGenerator 函数处于缺省状态,以及在键值生成规则中包含数据项索引 index。

结语

通过深入了解ForEach接口的使用方法和高级用法,我们可以更加灵活地处理不同场景下的数据渲染需求。在实际开发中,结合条件渲染逻辑和性能优化,能够提升用户体验并优化应用性能。希望本文对你理解和应用ArkTS中的ForEach接口提供了帮助。

ForEach接口是ArkUI框架中用于循环渲染数组类型数据的重要工具,通过合理的键值生成规则和组件创建规则,能够应对各种数据变化情况。在开发过程中,根据具体场景选择合适的使用方式,可以使界面动态展示和用户交互更加灵活和高效。

点击关注,第一时间了解华为云新鲜技术~

相关推荐
xiaodaoluanzha10 分钟前
迄今為止,最簡單的編程語言 Nolang
前端·后端
Csvn10 分钟前
Docker 容器管理入门 — 从镜像到容器编排
后端
用户7623524259115 分钟前
ShardingJDBC
后端
行者全栈架构师16 分钟前
IDEA 中 Maven 项目的 15 个红色报错快速解决方法
java·后端
Colin草率地做慢慢地改20 分钟前
关于QuickStore这个项目的重构(2)- 数据库建表文件
后端·面试·架构
IT_陈寒1 小时前
SpringBoot自动配置这个坑,我踩进去又爬出来了
前端·人工智能·后端
copyer_xyf2 小时前
Agent 流程编排
后端·python·agent
copyer_xyf2 小时前
Agent RAG
后端·python·agent
copyer_xyf2 小时前
【RAG】向量数据库:milvus
后端·python·agent
铁皮饭盒2 小时前
Bun 哪比 Node.js 快?
javascript·后端