Arclin

Advocate Technology. Enjoy Technology.

0%

鸿蒙状态管理装饰器V2

鸿蒙 API 12 推出了 V2 版本的状态管理装饰器,比起 V1 装饰器,V2 装饰器的职责更加地清晰,并且能力也得到了加强,建议后续开发使用 V2 版本的装饰器

相关文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/_u6001_u7ba1_u7406_uff08v2_u8bd5_u7528_u7248_uff09-V5

前提

V2 相关装饰器所涉及到的根视图组件都需要用@ComponentV2 进行修饰

@ObservedV2

举例:

问题

在 V1 中,@Observed 无法深度观测,比如以下的例子

  1. 先点击修改名字,名字变化,再点击修改班级,班级不会变化
  2. 先点击修改班级,班级不会变化,再点击修改名字,名字和班级同时发生变化

结论:

直接修改第二层的属性,虽然对象本身已经发生了变化,但是无法被@Observed 观测到
修改第一层的属性的时候,会被@Observed 观测到,执行 UI 刷新,并且刷新是全体的,并不只是修改绑定的属性对应的 UI
(这里会引申出一个性能问题:如果某个对象有很多属性,只改变一个属性会导致其他无关控件也被刷新,造成无意义的性能损耗)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@Observed
class Grade {
grade: number
name: string

constructor(grade: number, name: string) {
this.grade = grade
this.name = name
}
}

@Observed
class Student {
name: string
age: number
grade: Grade

constructor(name: string, age: number, grade: Grade) {
this.name = name
this.age = age
this.grade = grade
}
}

@Entry
@Component
struct Index {

@State student: Student = new Student("John",18, new Grade(3,"B"))

build() {
Column({ space: 20 }) {
Blank()
Text("Name: " + this.student.name)
.fontWeight(FontWeight.Bold)
Text("Age: " + this.student.age)
.fontWeight(FontWeight.Bold)
Row({ space: 20}) {
Text("Grade: " + this.student.grade.grade)
.fontWeight(FontWeight.Bold)
Text("Class: " + this.student.grade.name)
.fontWeight(FontWeight.Bold)
}
Button("修改名字")
.onClick(() => {
this.student.name = "Mike"
})
Button("修改班级")
.onClick(() => {
this.student.grade.name = "C"
})
Blank()
}
.width("100%")
.height('100%')
}
}

解决

被观察的类使用@ObservedV2 修饰,需要被观察到属性用@Trace 修饰即可

结果:

点击修改班级的时候,班级会发生变化;然而,点击修改名字的时候,名字不会变化

结论:

只有被@Trace 修饰的属性,修改的时候才会发生变化,没有@Trace 修饰的属性是不会被观察到的
使用该修饰器修饰的属性,被修改的时候只会重新渲染相关的组件,其他没有被修改的属性是不会重新渲染的,比起 V1 性能更好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@ObservedV2
class Grade {
grade: number
@Trace name: string

constructor(grade: number, name: string) {
this.grade = grade
this.name = name
}
}

@ObservedV2
class Student {
name: string
age: number
grade: Grade

constructor(name: string, age: number, grade: Grade) {
this.name = name
this.age = age
this.grade = grade
}
}

其他

@Trace 支持 model 嵌套,支持继承,支持修饰数组/Map/Date/Set

@Local

问题

  1. @Local 与@State 用法相同,区别在于@State 的属性可以外部赋值,而@Local 不允许

如果 student 用@State 修饰,下面代码是允许的,
如果 student 用@Local 修饰,则下面代码不允许

1
Index({ student: new Student("John",18, new Grade(3,"B")) })
  1. 上述的@ObserveV2 和@Trace 只能用于监听对象的属性,但是如果对象本身发生了变化,那 UI 不会刷新,如下代码点击“修改整体”, UI 不会刷新
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Entry
@ComponentV2
struct Index {

student: Student = new Student("John",18, new Grade(3,"B"))

build() {
Column({ space: 20 }) {
Blank()
Text("Name: " + this.student.name)
.fontWeight(FontWeight.Bold)
Text("Age: " + this.student.age)
.fontWeight(FontWeight.Bold)
Row({ space: 20}) {
Text("Grade: " + this.student.grade.grade)
.fontWeight(FontWeight.Bold)
Text("Class: " + this.student.grade.name)
.fontWeight(FontWeight.Bold)
}
Button("修改整体")
.onClick(() => {
this.student = new Student("Date", 23, new Grade(23, "None"))
})
Blank()
}
.width("100%")
.height('100%')
}
}

解决

使用@Local

1
@Local student: Student = new Student("John",18, new Grade(3,"B"))

@Param

搭配@Local 使用,用于父子组件数据单向传递,复杂类型比起@Prop 性能更好

@Require 表示必须外部传入,本地不用初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Entry
@ComponentV2
struct Index {

@Local student: Student = new Student("John",18, new Grade(3,"B"))

build() {
Column({ space: 20 }) {
Blank()
Info({ student: this.student })
Button("修改整体")
.onClick(() => {
this.student = new Student("Date", 23, new Grade(23, "None"))
})
Blank()
}
.width("100%")
.height('100%')
}
}

@ComponentV2
struct Info {
@Require @Param student: Student
build() {
Column({ space: 20 }) {
Text("Name: " + this.student.name)
.fontWeight(FontWeight.Bold)
Text("Age: " + this.student.age)
.fontWeight(FontWeight.Bold)
Row({ space: 20}) {
Text("Grade: " + this.student.grade.grade)
.fontWeight(FontWeight.Bold)
Text("Class: " + this.student.grade.name)
.fontWeight(FontWeight.Bold)
}
}
}
}

@Param 修饰的属性如果是对象的话,可以在子组件内部修改对象的成员属性,此时会同步到父组件
但是不可以修改对象本身

@Once

修饰@Param 修饰的属性,表示属性只能被初始化一次,后面不再改变,就算子组件或者父组件修改了这个属性,值也不会改变

1
2
3
4
5
6
@ComponentV2
struct Info {
@Require @Param student: Student;
@Once @Param type: number;
...
}

@Event

因为@Param 修饰的变量不可以在子组件修改,所以推出@Event 补充这个功能
使用方法是修饰一个方法类型,回调属性回去给父组件进行修改
@Event 只能修饰方法类型
@Event 更多只是作为一种规范,其实不加这个修饰符也行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Entry
@ComponentV2
struct Index {

@Local student: Student = new Student("John",18, new Grade(3,"B"))

build() {
Column({ space: 20 }) {
Blank()
Info({ student: this.student, change: (student: Student) => {
this.student = student
}})
Blank()
}
.width("100%")
.height('100%')
}
}

@ComponentV2
struct Info {
@Require @Param student: Student
@Event change: (x: Student) => void = () => {}
build() {
Column({ space: 20 }) {
Text("Name: " + this.student.name)
.fontWeight(FontWeight.Bold)
Text("Age: " + this.student.age)
.fontWeight(FontWeight.Bold)
.onClick(() => {
// 无效代码:this.student = new Student("Info", 33, new Grade(33, "Info What"))
this.change( new Student("Info", 33, new Grade(33, "Info What")) )
})
Row({ space: 20}) {
Text("Grade: " + this.student.grade.grade)
.fontWeight(FontWeight.Bold)
Text("Class: " + this.student.grade.name)
.fontWeight(FontWeight.Bold)
}.onClick(() => {
this.student.name = "Info Name"
})
}
}
}

!!

配合@Event 和@Param 进行父子组件双向绑定
子组件需要定义一个@Param 属性和一个同名@Event 并加上$,在点击按钮的时候调用@Event 的属性回调新值出去

1
2
3
4
5
6
7
8
9
10
11
12
@ComponentV2
struct Info {
@Require @Param student: Student;
@Event $student: (x: Student) => void = (x: Student) => {}
build() {
Button("Change Student")
.onClick(() => {
// 无效代码:this.student = new Student("Info", 33, new Grade(33, "Info What"))
this.$student(new Student("Info", 33, new Grade(33, "Info What")))
})
}
}

父组件传参数给子组件的时候加上!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Entry
@ComponentV2
struct Index {

@Local student: Student = new Student("John",18, new Grade(3,"B"))

build() {
Column({ space: 20 }) {
Blank()
Info({ student: this.student!! })
Blank()
}
.width("100%")
.height('100%')
}
}

实现效果为子组件修改传入的属性本身的时候,父子组件的属性值同时发生变化

@Computed

@Compute 可以把某个属性标记为计算属性,如下代码的 sum,注意此时 sum 不是方法,sum 依旧作为属性存在
当点击“修改 Grade”的时候,页面发生变化,实时显示 sum 计算后的结果,并且多次点击,不会重复执行 get 方法体
注意:sum 会发生变化的前提是,参与 sum 计算的属性需要被@Trace 修饰,如果参与 sum 计算的属性没有被监听的能力的话,那就不会触发计算方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Entry
@ComponentV2
struct Index {

@Local student: Student = new Student("John",18, new Grade(3,"B"))
@Computed get sum() {
let result = this.student.age + this.student.grade.grade
console.log(`result = ${result}`)
return result
}

build() {
Column({ space: 20 }) {
Blank()
Text(`sum ${this.sum}`)
.fontWeight(FontWeight.Medium)
Button("修改Grade")
.onClick(() => {
this.student.grade.grade = 30
})
Blank()
}
.width("100%")
.height('100%')
}
}

@Provider 和@Consumer

使用

  1. 用于父组件和子孙组件跨层级数据沟通,只要在同个层级树上,都可以获取到对应的数据
  2. 如下代码,点击 Change 按钮的时候,父组件和孙组件的 student2.name 都会更新 UI(前提是 name 属性加上了@Trace)

也可以直接更新 student2 本身
3. 因为是跨组件传递,所以即使 Info 组件不声明 student2 属性,OtherInfo 组件也可以通过@Comsumer 得到祖父组件的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Entry
@ComponentV2
struct Index {

@Local student: Student = new Student("John",18, new Grade(3,"B"))

@Provider() student2: Student = new Student("Mary",20, new Grade(4,"B"))

build() {
Column({ space: 20 }) {
Blank()
Text(`name: ${this.student2.name}`)
Text(`age: ${this.student2.age}`)
Info()
Blank()
}
.width("100%")
.height('100%')
}
}

@ComponentV2
struct Info {

@Consumer() student2: Student = new Student("",0,new Grade(0,""))

build() {
Column({ space: 20 }) {
OtherInfo()
Button("Change")
.onClick(() => {
this.student2.name = "Sarah"
})
}
}
}

@ComponentV2
struct OtherInfo {

@Consumer() student2: Student = new Student("",0,new Grade(0,""))

build() {
Row({space: 20}) {
Text(`name: ${this.student2.name}`)
Text(`age: ${this.student2.age}`)
}
.backgroundColor(Color.Red)
}
}

别名

@Provider 加上别名改为 stu, 那么子组件也要改成 stu 才能访问得到,如果还是使用 student2,则反问不到
同理@Consumer 也可以加上别名,那么孙组件的@Comsumer 就得对应改成相应的别名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Entry
@ComponentV2
struct Index {

@Provider('stu') student2: Student = new Student("Mary",20, new Grade(4,"B"))

build() {
Column({ space: 20 }) {
Blank()
Text(`name: ${this.student2.name}`)
Text(`age: ${this.student2.age}`)
Info()
Blank()
}
.width("100%")
.height('100%')
}
}

@ComponentV2
struct Info {

@Consumer() stu: Student = new Student("",0,new Grade(0,""))

build() {
Column({ space: 20 }) {
OtherInfo()
Button("Change")
.onClick(() => {
this.student2 = new Student("333",0,new Grade(0,"222"))
})
}
}
}

@Monitor

介绍

可以在类或者结构体中监听某个属性或者某个对象的属性,只能修饰方法,并且方法固定一个参数为 IMoitor
相比起 V1 的@Watch 功能更加强大,可以获取到更新前后的值
IMonitor 类型: IMonitor 类型的变量用作@Monitor 装饰方法的参数。

属性 类型 参数 返回值 说明
dirty Array 保存发生变化的属性名。
value function path?: string IMonitorValue 获得指定属性(path)的变化信息。当不填 path 时返回@Monitor 监听顺序中第一个改变的属性的变化信息。

IMonitorValue类型: IMonitorValue类型保存了属性变化的信息,包括属性名、变化前值、当前值。

属性 类型 说明
before T 监听属性变化之前的值。
now T 监听属性变化之后的当前值。
path string 监听的属性名。

使用

  1. 使用@Monitor 加上要监听的属性名,如果是监听对象的属性,就使用点语法(需要属性被@Trace 修饰)
  2. 监听数组的时候可以用.0、.1 的方式表示要监听的内容下标,比如@Monitor(“dimensionTwo.0.0”, “dimensionTwo.1.1”)
  3. 监听多个属性用逗号分隔
  4. 可以用多个@Monitor,但是如果监听同个属性,那么只有最后一个方法会生效
  5. 如果监听的对象变了,但是对象里面的所有属性都与变化前的一致,那么不会触发监听回调
  6. 监听时标记的要监听的属性字符串需要初始化时确定,初始化后无法修改监听的属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Entry
@ComponentV2
struct Index {

@Local student: Student = new Student("John",18, new Grade(3,"B"))

@Local count: number = 20

@Monitor("student.name", "count")
onChange(monitor: IMonitor) {
monitor.dirty.forEach((path: string) => {
console.log(`${path} change from ${monitor.value(path)?.before} to ${monitor.value(path)?.now}`)
})
}

build() {
Column({ space: 20 }) {
Blank()
Text("Name: " + this.student.name)
.fontWeight(FontWeight.Bold)
Button("修改count")
.onClick(() => {
this.count += 1
this.student.age = this.count
this.student.name = `name_${this.count}`
})
Blank()
}
.width("100%")
.height('100%')
}
}

@Type

当持久化数据的时候,遇到嵌套属性,需要标记为@Type,目的是反序列化的时候能够保持原来的类型信息
这样子使用 PersistenceV2 获取数据的时候能够维持原数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { Type } from '@kit.ArkUI';

// 数据中心
@ObservedV2
class SampleChild {
@Trace p1: number = 0;
p2: number = 10;
}

@ObservedV2
export class Sample {
// 对于复杂对象需要@Type修饰,确保序列化成功
@Type(SampleChild)
@Trace f: SampleChild = new SampleChild();
}
import { PersistenceV2 } from '@kit.ArkUI';
import { Sample } from '../Sample';

@Entry
@ComponentV2
struct Page {
prop: Sample = PersistenceV2.connect(Sample, () => new Sample())!;

build() {
Column() {
Text(`Page1 add 1 to prop.p1: ${this.prop.f.p1}`)
.fontSize(30)
.onClick(() => {
this.prop.f.p1++;
})
}
}
}