目录
- 前言
- 状态管理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 = outClassName
this.innerClass = new InnerClass(`${outClassName}_innerClass`)
}
outClassName: string
innerClass: 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.grade
this.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.person1
AddAge.addAge(tmp)
}).padding(10).margin(10)
Text(`person2.add:${this.person2.age}`).onClick((_) => {
let tmp = this.person2
new AddAge1().addAge(tmp)
}).padding(10).margin(10)
Text(`person3.add:${this.person3.age}`).onClick((_) => {
let tmp = this.person3
this.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: number
name: string
address: Address
education: Education
constructor(id: number, name: string, address: Address, education: Education) {
this.id = id;
this.name = name;
this.address = address;
this.education = education;
}
}
@Observed
class Address {
zipCode: number
location: string
constructor(zipCode: number, location: string) {
this.zipCode = zipCode;
this.location = location;
}
}
class Education {
school: string
degree: string
constructor(school: string, degree: string) {
this.school = school;
this.degree = degree;
}
}
这里还有一点需要注意的,到目前为止@ObjectLink
必须在自定义组件中使用。我们这里定义两个组件,用来展示Address
和Education
.
@Component
struct UserAddress {
@ObjectLink address: Address
build() {
Column() {
Text(`location:${this.address.location}`)
Text(`zipCode:${this.address.zipCode}`)
}.margin(10).padding(5)
}
}
@Component
struct UserEducation {
@Prop education: Education
build() {
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: string
secondLevel: SecondLevel
constructor(name: string, secondLevel: SecondLevel) {
this.name = name;
this.secondLevel = secondLevel;
}
}
@Observed
class SecondLevel {
name: string
thirdLevel: ThirdLevel
constructor(name: string, thirdLevel: ThirdLevel) {
this.name = name;
this.thirdLevel = thirdLevel;
}
}
@Observed
class ThirdLevel {
name: string
constructor(name: string) {
this.name = name;
}
}
然后我们定义两个用来展示SecondLevel
和ThirdLevel
的控件
@Component
struct SecondLevelView{
@ObjectLink secondLevel:SecondLevel
build() {
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:ThirdLevel
build() {
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的数据源。这个过程不是在父组件数据源变化后立刻发生的,而是在父组件实际刷新时才会进行。