鸿蒙-状态管理V1

devtools/2025/2/24 21:17:51/

目录

    • 前言
    • 状态管理V1
      • @State装饰器
        • 初始化
        • 观察能力
        • 小坑
      • @Prop装饰器 和 @Link装饰器
      • @Observed装饰器和@ObjectLink装饰器
        • 使用示例
        • 小结

前言

随着鸿蒙Next的推广,做鸿蒙开发的人是越来越多,提问和寻求帮助的人也是越来越多,就我自己回答的问题而言,大部分和状态管理相关,比如List刷新问题,,还有一些录音录像拍照问题。也不是太难的问题,需要特别仔细的阅读官方文档,有些问题的解决方法还分散在好几个文档里面,文档上也没有对一些关键点做特别讲解。这里就最常见的问题总结一下,希望后来的朋友少走一些弯路。

状态管理V1

组件的状态管理一共就这几个:
@State装饰器:组件内状态
@Prop装饰器:父子单向同步
@Link装饰器:父子双向同步
@Observed装饰器和@ObjectLink装饰器:嵌套类对象属性变化
@Provide装饰器和@Consume装饰器:与后代组件双向同步
其中
@Provide和@Consume群里也几乎没有人问过,姑且认为是大家都比较清楚应该怎么使用或者用的较少

@State装饰器

初始化

必须本地初始化。如果从父组件初始化,并且从父组件传入的值非undefined,将会覆盖本地初始化;如果从父组件传入的值为undefined,则初值为@State装饰变量自身的初值。
也就是说无论父组件是否传值,被@State修饰的变量都必须要指定初始值,即使被@Require修饰也必须指定初始值。

观察能力

能观察到简单对象的变化;能观察到class 或者 Object 的赋值变化;能观察到其属性赋值的变化,但无法观察嵌套类属性赋值的变化;
看个例子,假设我们的数据类是这样的:

class OutClass {constructor(outClassName: string) {this.outClassName = outClassNamethis.innerClass = new InnerClass(`${outClassName}_innerClass`)}outClassName: stringinnerClass: InnerClass
}class InnerClass {constructor(innerClassName: string) {this.innerClassName = innerClassName}innerClassName: string
}

我们的业务逻辑姑且简化为这样


@Component
struct AboutStateAnno {@State count: number = 0@State grade: string = 'a'@State success: boolean = false@State outClass: OutClass = new OutClass("out_class")build() {Column() {Text(`count: ${this.count}`)Text(`grade: ${this.grade}`)Text(`success: ${this.success}`)Text(`out_class_name: ${this.outClass.outClassName}`)Text(`inner_class_name: ${this.outClass.innerClass.innerClassName}`)Button('改变简单数据,UI刷新').onClick((_)=>{this.count ++this.grade += this.gradethis.success = !this.success})Button('改变outClassName,UI刷新').onClick((_)=>{this.outClass.outClassName = 'out_afterChange'})Button('改变innerClassName,UI不刷新').onClick((_)=>{this.outClass.innerClass.innerClassName = 'inner_afterChange'})Button('改变innerClass,UI刷新').onClick((_)=>{this.outClass.innerClass = new InnerClass('new inner_class')})}.margin(10).borderRadius(10).backgroundColor(Color.Gray).padding(10)}
}

可以看到,改变简单数据后,UI 直接刷新了。修改对象中的属性,UI 也刷新了,但是在改变嵌套类属性时(改变innerClassName,UI不刷新),UI 没有刷新,也就是说改变Object.keys(observedObject)返回的所有属性时,UI都可以刷新。
同样的,对于数组类型来讲:数组的整体赋值,数组项的赋值,调用 pop 和 push 也能观察到。
对于Map来讲:可以观察到Map整体的赋值,同时可通过调用Map的接口set, clear, delete 更新Map的值。
对于Set来讲:可以观察到Set整体的赋值,同时可通过调用Set的接口add, clear, delete 更新Set的值。
这里就不再过多赘述。

小坑

但这里有个比较麻烦的地方:当在build方法内,当@State装饰的变量是Object类型、且通过a.b(this.object)形式调用时,修改对象中的属性后,UI 无法刷新
比如有一个 Person 类,有个 age 属性,我们定义一个方法用来修改 age 属性。我能想到的大概有这么 5 种方式

class Person {age: number = 0
}
//方法 1:定义一个工具类,写个静态方法,传入 person 对象
class AddAge {static addAge(person: Person) {person.age += 2}
}
//方法 2:定义一个工具类,写个普通方法,传入 person 对象
class AddAge1 {addAge(person: Person) {person.age += 2}
}
//方法 3:在组件中定义一个方法,传入 person 对象
addAge1(person: Person) {person.age += 2
}
//方法 4:在组件中定义一个方法,直接修改 person 对象
addAge() {this.person.age += 2
}
//方法 5:在需要的地方直接修改 person 对象的 age 属性,比如在onClick事件中
Text(`person.add:${this.person.age}`).onClick((_) => {this.person.age += 2;
})

我们来试一下:

@State person1: Person = new Person()
@State person2: Person = new Person()
@State person3: Person = new Person()
@State person4: Person = new Person()
@State person5: Person = new Person()
build() {Column() {Text(`person1.add: ${this.person1.age}`).onClick((_) => {AddAge.addAge(this.person1)}).padding(10).margin(10)Text(`person2.add:${this.person2.age}`).onClick((_) => {new AddAge1().addAge(this.person2)}).padding(10).margin(10)Text(`person3.add:${this.person3.age}`).onClick((_) => {this.addAge1(this.person3)}).padding(10).margin(10)Text(`person4.add:${this.person4.age}`).onClick((_) => {this.addAge()}).padding(10).margin(10)Text(`person5.add:${this.person5.age}`).onClick((_) => {this.person5.age += 2;}).padding(10).margin(10)
}.margin(10)
}

会发现只有方法 4 和方法 5 才能使得UI 刷新,这是因为方法内传过去的是原生对象,修改其属性后不能触发刷新。如果我们有一些理由非得这么做,可以先赋值给一个临时变量,再将这个临时变量传入方法中,就可以刷新 UI 了。

  build() {Column() {Column() {Text(`person1.add: ${this.person1.age}`).onClick((_) => {let tmp = this.person1AddAge.addAge(tmp)}).padding(10).margin(10)Text(`person2.add:${this.person2.age}`).onClick((_) => {let tmp = this.person2new AddAge1().addAge(tmp)}).padding(10).margin(10)Text(`person3.add:${this.person3.age}`).onClick((_) => {let tmp = this.person3this.addAge1(tmp)}).padding(10).margin(10)Text(`person4.add:${this.person4.age}`).onClick((_) => {this.addAge()}).padding(10).margin(10)Text(`person5.add:${this.person5.age}`).onClick((_) => {this.person5.age += 2;}).padding(10).margin(10)}.margin(10)}.height('100%').width('100%')}

没想到吧,哈哈哈哈哈

@Prop装饰器 和 @Link装饰器

这两个装饰器的观察能力和坑点和@State 相同,只不过是用来自定义的子组件中。
其中@Prop装饰器是父组件向子组件同步数据:子组件改变数据不会同步到父组件,但父组件修改数据会同步到子组件。也就是说父组件的修改会覆盖掉子组件对变量的修改,并且被它装饰的变量会进行深拷贝,在拷贝的过程中除了基本类型、Map、Set、Date、Array外,都会丢失类型。因此建议深度嵌套的数据不要太多,文档上建议不要超过 5 层。
@Link装饰器是父子组件双向同步的,但禁止本地进行初始化。

@Observed装饰器和@ObjectLink装饰器

在实际开发中,我们使用的数据模型一般都会有多层嵌套的情况,但之前介绍过的装饰器只能观察到第一层变化,无法观察到第二层变化,这种情况我们就需要使用@Observed@ObjectLink装饰器.

使用示例

@Observed装饰class。需要放在class的定义前,使用new创建类对象。
@ObjectLink变量装饰器只能装饰被@Observed装饰的class实例,必须指定类型,并且不支持简单类型。

另外这两个装饰器是配套使用的,单一的使用某个装饰器无法实现观察,并且@ObjectLink只能装饰被@Observed装饰的类对象变量,否则会报错

The ‘@ObjectLink’ decorated attribute ‘education’ must be an ‘@Observed’ decorated class or a union of ‘@Observed’ decorated class and undefined or null, or both.

ObjectLink修饰没有被Observed装饰的类对象

我们先定义一个数据类,来模拟一下业务逻辑:


@Observed
class User {id: numbername: stringaddress: Addresseducation: Educationconstructor(id: number, name: string, address: Address, education: Education) {this.id = id;this.name = name;this.address = address;this.education = education;}
}@Observed
class Address {zipCode: numberlocation: stringconstructor(zipCode: number, location: string) {this.zipCode = zipCode;this.location = location;}
}class Education {school: stringdegree: stringconstructor(school: string, degree: string) {this.school = school;this.degree = degree;}
}

这里还有一点需要注意的,到目前为止@ObjectLink必须在自定义组件中使用。我们这里定义两个组件,用来展示AddressEducation.

@Component
struct UserAddress {@ObjectLink address: Addressbuild() {Column() {Text(`location:${this.address.location}`)Text(`zipCode:${this.address.zipCode}`)}.margin(10).padding(5)}
}@Component
struct UserEducation {@Prop education: Educationbuild() {Column() {Text(`school:${this.education.school}`)Text(`degree:${this.education.degree}`)}.margin(10).padding(5)}
}

简单的写个页面,看下效果。

@State user: User | undefined = undefined
aboutToAppear(): void {//这里必须使用 new 关键字创建对象,使用字面量创建出来的对象无法被观察let address: Address = new Address(10000, "北京")let education: Education = new Education('university', 'Master')this.user = new User(1, 'new User', address, education) 
}
build() {Column() {if (this.user) {Text(`id:${this.user.id} , name:${this.user.name}`)UserAddress({ address: this.user.address })UserEducation({ education: this.user.education })Flex({ wrap: FlexWrap.Wrap, space: { cross: LengthMetrics.vp(5), main: LengthMetrics.vp(5) } }) {Button('修改 name').onClick((_) => {if (this.user) {this.user.name += 'a'}})Button('修改address.location').onClick((_) => {if (this.user) {this.user.address.location += 'b'}})Button('修改education.degree').onClick((_) => {if (this.user) {this.user.education.degree += 'c'}})}}}
}

可以看到,点击修改 name修改address.location都可以引起 UI 刷新,但点击修改education.degreeUI不会刷新。

另外还有一点需要注意的:被@Observed 修饰的嵌套类,不能跨嵌套层观察。举个例子:我们有一个嵌套的三层的数据类,我们是无法在第二层的自定义控件中观察到第三层数据的变化。示例如下:

//嵌套了三层的数据类:也就是A类中有类型为B的属性,B类中又有类型为C的属性 
@Observed
class FirstLevel {name: stringsecondLevel: SecondLevelconstructor(name: string, secondLevel: SecondLevel) {this.name = name;this.secondLevel = secondLevel;}
}@Observed
class SecondLevel {name: stringthirdLevel: ThirdLevelconstructor(name: string, thirdLevel: ThirdLevel) {this.name = name;this.thirdLevel = thirdLevel;}
}
@Observed
class ThirdLevel {name: stringconstructor(name: string) {this.name = name;}
}

然后我们定义两个用来展示SecondLevelThirdLevel的控件

@Component
struct SecondLevelView{@ObjectLink secondLevel:SecondLevelbuild() {Column(){Text(`second level:${this.secondLevel.name}`)//这里直接跨层级观察是无效的,修改thirdLevel.name时 UI 不会刷新Text(`third level name:${this.secondLevel.thirdLevel.name}`)ThirdLevelView({thirdLevel:this.secondLevel.thirdLevel}).backgroundColor('#846f9b').margin(10)}.margin(10)}
}
@Component
struct ThirdLevelView{@ObjectLink thirdLevel:ThirdLevelbuild() {Text(`third level: ${this.thirdLevel.name}`)}
}

接着写个页面试一下

@State firstLevel:FirstLevel | undefined = undefined
aboutToAppear(): void {let thirdLevel:ThirdLevel = new ThirdLevel('third level')let secondLevel:SecondLevel = new SecondLevel('second level',thirdLevel)this.firstLevel = new FirstLevel('first level',secondLevel)
}
build() {Column() {if(this.firstLevel){Column(){Text(`first level:${this.firstLevel.name}`)SecondLevelView({secondLevel:this.firstLevel.secondLevel}).backgroundColor('#aa42f5').margin(10)Flex({ wrap: FlexWrap.Wrap, space: { cross: LengthMetrics.vp(5), main: LengthMetrics.vp(5) } }) {Button('修改 firstLevel.name').onClick((_)=>{if(this.firstLevel){this.firstLevel.name  += 'a'}})Button('修改 secondLevel.name').onClick((_)=>{if(this.firstLevel){this.firstLevel.secondLevel.name  += 'a'}})Button('修改 thirdLevel.name').onClick((_)=>{if(this.firstLevel){this.firstLevel.secondLevel.thirdLevel.name  += 'a'}})}.margin(10)}.margin(10).padding(10).backgroundColor("#657e57")}}
}

跨嵌套层级观察失效

可以看到,在对应的层级观察对应的嵌套类是生效的,但当我们点击修改 thirdLevel.name时,只有在ThirdLevelView中的控件刷新了,在SecondLevelView中观察thirdLevel.name的 Text内容并没有刷新。这是因为在SecondLevelView中,@ObjectLink仅能观察到其代理的secondLevel:SecondLevel对象的属性变化,而secondLevel.thirdLevel.nameThirdLevel的属性,无法观察到嵌套类的变化。

小结
  • 不建议在被@Observed 修饰的类的构造函数中修改值,不会引起UI刷新,因为修改的是原始对象的值,并非是代理对象。
  • @Observed修饰的嵌套类,不要跨嵌套层观察,建议一个数据类对应一个自定义控件。
  • 必须使用new 创建的类对象,使用字面量对象无法被观察。比如网络请求返回的数据转成 JSON 后使用 as 强转为对应类型
  • 通过a.b(this.object)形式调用时,修改对象中的属性后,UI 无法刷新,原因同@State
  • @ObjectLink的数据源更新依赖其父组件,当父组件中数据源改变引起父组件刷新时,会重新设置子组件@ObjectLink的数据源。这个过程不是在父组件数据源变化后立刻发生的,而是在父组件实际刷新时才会进行。

http://www.ppmy.cn/devtools/161437.html

相关文章

C#中级教程(1)——解锁 C# 编程的调试与错误处理秘籍

一、认识错误:编程路上的 “绊脚石” 在 C# 编程中,错误大致可分为两类:语法错误和语义错误(逻辑错误)。语法错误就像是写作文时的错别字和病句,编译器一眼就能识别出来,比如变量名拼写错误、符…

Service Mesh在爱奇艺的落地实践:架构、运维与扩展

在当前的数字化时代,微服务架构已经成为企业技术栈的重要组成部分。然而,随着微服务数量的增加,服务治理的复杂性也随之增长。爱奇艺作为一家领先的在线视频平台,面临着微服务治理的挑战,如缺乏统一治理标准、部署运维…

基于spring boot物流管理系统设计与实现(代码+数据库+LW)

摘 要 社会发展日新月异,用计算机应用实现数据管理功能已经算是很完善的了,但是随着移动互联网的到来,处理信息不再受制于地理位置的限制,处理信息及时高效,备受人们的喜爱。本次开发一套物流管理系统有管理员和用户…

HTML中,title和h1标签的区别是什么?

在 HTML 中,title和h1标签虽然都与文本内容展示相关,但它们的用途、位置和作用有明显的区别,下面为你详细介绍: 1. 用途 title标签:主要用于定义整个 HTML 文档的标题,这个标题通常显示在浏览器的标题栏或…

docker下安装 es 设置账号密码

环境 ElasticSearch版本:7.6.2 步骤 使用docker命令进入es容器 修改es的配置文件:elasticsearch.yml,添加如下配置 xpack.security.enabled: true xpack.license.self_generated.type: basic xpack.security.transport.ssl.enabled: tru…

C++双指针:算法优化的“左右互搏术”与高效问题破解全指南

C双指针:算法优化的“左右互搏术”与高效问题破解全指南 开篇故事:迷宫中的“双人探路策略” 想象两名探险者在迷宫中寻找出口: 快慢指针:一人快速探索死路,另一人稳步记录正确路径。左右指针:两人从两端…

蓝桥杯试题:小明的彩灯(差分 前缀和)

一、题目描述 小明拥有 N 个彩灯,第 ii个彩灯的初始亮度为 ai​。 小明将进行 Q次操作,每次操作可选择一段区间,并使区间内彩灯的亮度 x(x 可能为负数)。 求 QQ次操作后每个彩灯的亮度(若彩灯亮度为负数…

[AI]【Comfyui】 生成基本流程图的步骤保姆记录

在进行深度学习模型或图像生成的过程中,创建流程图能够帮助清晰地表达模型的工作流程和数据流动。本文将为您介绍生成基本流程图的一般步骤,适用于常见的深度学习图像生成模型。以下是该流程图的基本步骤: 1. 创建 Load Checkpoint 节点 流程图的第一步通常是加载已经训练好…