目录
- 前言
- 状态管理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.
我们先定义一个数据类,来模拟一下业务逻辑:
@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
必须在自定义组件中使用。我们这里定义两个组件,用来展示Address
和Education
.
@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.degree
UI不会刷新。
另外还有一点需要注意的:被@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;}
}
然后我们定义两个用来展示SecondLevel
和ThirdLevel
的控件
@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.name
是ThirdLevel
的属性,无法观察到嵌套类的变化。
小结
- 不建议在被@Observed 修饰的类的构造函数中修改值,不会引起UI刷新,因为修改的是原始对象的值,并非是代理对象。
- 被
@Observed
修饰的嵌套类,不要跨嵌套层观察,建议一个数据类对应一个自定义控件。 - 必须使用new 创建的类对象,使用字面量对象无法被观察。比如网络请求返回的数据转成 JSON 后使用 as 强转为对应类型
- 通过a.b(this.object)形式调用时,修改对象中的属性后,UI 无法刷新,原因同@State
- @ObjectLink的数据源更新依赖其父组件,当父组件中数据源改变引起父组件刷新时,会重新设置子组件@ObjectLink的数据源。这个过程不是在父组件数据源变化后立刻发生的,而是在父组件实际刷新时才会进行。