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

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

ForEach接口概述

介绍

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

接口描述

ForEach(

arr: Array,

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

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

)

参数说明

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

键值生成规则

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

默认规则

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

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

组件创建规则

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

}

}

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

非首次渲染

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

@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的重新渲染。

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

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

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

高级用法

条件渲染逻辑

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

@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在初始化渲染时不会一次性加载所有数据,而是根据滚动位置和用户操作,动态地加载可见区域的数据项。这有助于提高页面的渲染性能。以下是一个简单的示例代码:

@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项后插入新项”文本组件后,界面会出现非预期的结果。

@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个数组项以及其后的所有项重新创建组件。

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

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

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

  1. Python中的Django框架中prefetch_related()函数对数据库查询的优化

    实例的背景说明 假定一个个人信息系统,需要记录系统中各个人的故乡.居住地.以及到过的城市.数据库设计如下: Models.py 内容如下: ? 1 2 3 4 5 6 7 8 9 10 11 12 1 ...

  2. web性能优化之---JavaScript中的无阻塞加载性能优化方案

    一.js阻塞特性 JS 有个很无语的阻塞特性,就是当浏览器在执行JS 代码时,不能同时做其他任何事情,无论其代码是内嵌的还是外部的. 即<script>每次出现都会让页面等待脚本的解析和执 ...

  3. 《高性能SQL调优精要与案例解析》一书谈主流关系库SQL调优(SQL TUNING或SQL优化)核心机制之——索引(index)

    继<高性能SQL调优精要与案例解析>一书谈SQL调优(SQL TUNING或SQL优化),我们今天就谈谈各主流关系库中,占据SQL调优技术和工作半壁江山的.最重要的核心机制之一——索引(i ...

  4. 《高性能SQL调优精要与案例解析》一书谈SQL调优(SQL TUNING或SQL优化)学习

    <高性能SQL调优精要与案例解析>一书上市发售以来,很多热心读者就该书内容及一些具体问题提出了疑问,因读者众多外加本人日常工作的繁忙 ,在这里就SQL调优学习进行讨论并对热点问题统一作答. ...

  5. 基于AngularJS/Ionic框架开发的性能优化

    AngularJS作为强大的前端MVVM框架,虽然已经做了很多的性能优化,但是我们开发过程中的不当使用还是会对性能产生巨大影响. 下面提出几点优化的方法: 1. 使用单次绑定符号{{::value}} ...

  6. asp.net MVC 框架中控制器里使用Newtonsoft.Json对前端传过来的字符串进行解析

    下面我用一个实例来和大家分享一下我的经验,asp.net MVC 框架中控制器里使用Newtonsoft.Json对前端传过来的字符串进行解析. using Newtonsoft.Json; usin ...

  7. php中foreach()函数与Array数组经典案例讲解

    //php中foreach()函数与Array数组经典案例讲解 function getVal($v) { return $v; //可以加任意检查代码,列入要求$v必须是数字,或过滤非法字符串等.} ...

  8. 关于HttpSession 和 Hibernate框架中 session异同点的简单解析

    快速理解: HttpSession中的session是一个容器用来盛基于会话机制的信息. 比喻:我把钱放进银行的保险柜里. 解析:我的钱就是我的信息,ID等 银行的保险柜就是session容器. Hi ...

  9. Spring5源码解析-Spring框架中的单例和原型bean

    Spring5源码解析-Spring框架中的单例和原型bean 最近一直有问我单例和原型bean的一些原理性问题,这里就开一篇来说说的 通过Spring中的依赖注入极大方便了我们的开发.在xml通过& ...

  10. 在ASP.NET MVC 框架中调用 html文件及解析get请求中的参数值

    在ASP.NET MVC 框架中调用 html文件: public ActionResult Index() { using (StreamReader sr = new StreamReader(P ...

随机推荐

  1. CF1336A

    题目简化和分析: 明确一点这是一棵树. 为了保证每个工业城市的设置效益最大,应该设在最深的节点. 从深到浅,可以使用优先队列去实现. 设置一个的价值为 \(dep_u-siz_u-1\). 关于作者一 ...

  2. 关于Xftp7和Xshell7是最新版本但是报错这件事

    以win11为例,仅供参考.只能说捣鼓这么久还是建议各位转用MobaXterm. 先说一下,我的解决方法只能说是跳过问题,不是解决问题. <要继续使用此程序 您必须应用最新的更新或使用新版本&g ...

  3. NEFU OJ Problem1356 帽儿山奇怪的棋盘 题解

    帽儿山奇怪的棋盘 题目: Time Limit:1000ms | Memory Limit:65535K Description 军哥来到了帽儿山,发现有两位神人在顶上对弈.棋盘长成下图的模样: 每个 ...

  4. ORACLE SQL语句逻辑读高优化案例

    川川找我优化SQL,逻辑读达到398,000,安排一下. SQL和执行计划: SELECT t1.*, t3.bed_number, t3.patient_name, t4.name FROM odw ...

  5. 一款实用的.NET Core加密解密工具类库

    前言 在我们日常开发工作中,为了数据安全问题对数据加密.解密是必不可少的.加密方式有很多种如常见的AES,RSA,MD5,SAH1,SAH256,DES等,这时候假如我们有一个封装的对应加密解密工具类 ...

  6. 老知识复盘-SQL从提交到执行到底经历了什么

    一.什么是SQL sql(Structured Query Language: 结构化查询语言)是高级的费过程化编程语言,允许用户在高层数据结构上工作, 是一种数据查询和程序设计语言, 也是(ANSI ...

  7. HTML5实战—canvas绘图之贝塞尔曲线

    1.二次贝塞尔曲线 quadraticCurveTo(cpx,cpy,x,y) //cpx,cpy表示控制点的坐标, x,y表示终点坐标: 数学公式表示如下: 二次方贝兹曲线的路径由给定点P0.P1. ...

  8. Python 中的单下划线和双下划线

    哈喽大家好,我是咸鱼 当我们在学习 Python 的时候,可能会经常遇到单下划线 _ 和双下划线 __ 这两种命名方式 单下划线 _ 和双下划线 __ 不仅仅是只是一种简单的命名习惯,它们在 Pyth ...

  9. 数据库系列:MySQL不同操作分别用什么锁?

    数据库系列:MySQL慢查询分析和性能优化 数据库系列:MySQL索引优化总结(综合版) 数据库系列:高并发下的数据字段变更 数据库系列:覆盖索引和规避回表 数据库系列:数据库高可用及无损扩容 数据库 ...

  10. HarmonyOS第一课_构建更加丰富的页面

    管理组件状态 概述 组件内的状态管理:@State 从父组件单向同步状态:@Prop 与父组件双向同步状态:@Link 跨组件层级双向同步状态:@Provide 和@Consume 概述 在应用中,界 ...