android2harmony 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/self-tester.md +376 -0
- package/dist/index.js +152 -7
- package/dist/index.js.map +4 -4
- package/package.json +32 -29
- package/skills/hmos-incremental-ui-align/README.md +251 -0
- package/skills/hmos-incremental-ui-align/SKILL.md +365 -0
- package/skills/hmos-incremental-ui-align/diff_analysis.md +53 -0
- package/skills/hmos-incremental-ui-align/page_align.md +62 -0
- package/skills/hmos-incremental-ui-align/references/Comparison_Template.md +38 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/@Link/350/243/205/351/245/260/345/231/250/357/274/232/347/210/266/345/255/220/345/217/214/345/220/221/345/220/214/346/255/245.md +648 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/@Observed/350/243/205/351/245/260/345/231/250/345/222/214@ObjectLink/350/243/205/351/245/260/345/231/250/357/274/232/345/265/214/345/245/227/347/261/273/345/257/271/350/261/241/345/261/236/346/200/247/345/217/230/345/214/226.md +2089 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/@Prop/350/243/205/351/245/260/345/231/250/357/274/232/347/210/266/345/255/220/345/215/225/345/220/221/345/220/214/346/255/245.md +1033 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/@Provide/350/243/205/351/245/260/345/231/250/345/222/214@Consume/350/243/205/351/245/260/345/231/250/357/274/232/344/270/216/345/220/216/344/273/243/347/273/204/344/273/266/345/217/214/345/220/221/345/220/214/346/255/245.md +1183 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/@State/350/243/205/351/245/260/345/231/250/357/274/232/347/273/204/344/273/266/345/206/205/347/212/266/346/200/201.md +576 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/@Track/350/243/205/351/245/260/345/231/250/357/274/232class/345/257/271/350/261/241/345/261/236/346/200/247/347/272/247/346/233/264/346/226/260.md +297 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/@Watch/350/243/205/351/245/260/345/231/250/357/274/232/347/212/266/346/200/201/345/217/230/351/207/217/346/233/264/346/224/271/351/200/232/347/237/245.md +395 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/AppStorage/357/274/232/345/272/224/347/224/250/345/205/250/345/261/200/347/232/204UI/347/212/266/346/200/201/345/255/230/345/202/250.md +903 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/Environment/357/274/232/350/256/276/345/244/207/347/216/257/345/242/203/346/237/245/350/257/242.md +106 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/LocalStorage/357/274/232/351/241/265/351/235/242/347/272/247UI/347/212/266/346/200/201/345/255/230/345/202/250.md +1178 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/MVVM/346/250/241/345/274/217V1.md +911 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243/PersistentStorage/357/274/232/346/214/201/344/271/205/345/214/226/345/255/230/345/202/250UI/347/212/266/346/200/201.md +355 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243//347/256/241/347/220/206/345/272/224/347/224/250/346/213/245/346/234/211/347/232/204/347/212/266/346/200/201/346/246/202/350/277/260.md +11 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243V2/!!/350/257/255/346/263/225/357/274/232/345/217/214/345/220/221/347/273/221/345/256/232.md +206 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243V2/@Computed/350/243/205/351/245/260/345/231/250/357/274/232/350/256/241/347/256/227/345/261/236/346/200/247.md +373 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243V2/@Event/350/243/205/351/245/260/345/231/250/357/274/232/350/247/204/350/214/203/347/273/204/344/273/266/350/276/223/345/207/272.md +158 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243V2/@Local/350/243/205/351/245/260/345/231/250/357/274/232/347/273/204/344/273/266/345/206/205/351/203/250/347/212/266/346/200/201.md +750 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243V2/@Monitor/350/243/205/351/245/260/345/231/250/357/274/232/347/212/266/346/200/201/345/217/230/351/207/217/344/277/256/346/224/271/345/274/202/346/255/245/347/233/221/345/220/254.md +1704 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243V2/@ObservedV2/350/243/205/351/245/260/345/231/250/345/222/214@Trace/350/243/205/351/245/260/345/231/250/357/274/232/347/261/273/345/261/236/346/200/247/345/217/230/345/214/226/350/247/202/346/265/213.md +1012 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243V2/@Once/350/243/205/351/245/260/345/231/250/357/274/232/345/210/235/345/247/213/345/214/226/345/220/214/346/255/245/344/270/200/346/254/241.md +164 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243V2/@Param/350/243/205/351/245/260/345/231/250/357/274/232/347/273/204/344/273/266/345/244/226/351/203/250/350/276/223/345/205/245.md +840 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243V2/@Provider/350/243/205/351/245/260/345/231/250/345/222/214@Consumer/350/243/205/351/245/260/345/231/250/357/274/232/350/267/250/347/273/204/344/273/266/345/261/202/347/272/247/345/217/214/345/220/221/345/220/214/346/255/245.md +856 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243V2/@Type/350/243/205/351/245/260/345/231/250/357/274/232/346/240/207/350/256/260/347/261/273/345/261/236/346/200/247/347/232/204/347/261/273/345/236/213.md +83 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243V2/AppStorageV2/357/274/232/345/272/224/347/224/250/345/205/250/345/261/200UI/347/212/266/346/200/201/345/255/230/345/202/250.md +294 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243V2/MVVM/346/250/241/345/274/217/357/274/210V2/357/274/211.md +1407 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243V2/PersistenceV2/357/274/232/346/214/201/344/271/205/345/214/226/345/255/230/345/202/250UI/347/212/266/346/200/201.md +1220 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243V2/_/350/214/203/345/274/217/351/200/211/346/213/251/350/257/264/346/230/216.md +47 -0
- package/skills/hmos-incremental-ui-align/references/MVVM/345/274/200/345/217/221/346/226/207/346/241/243V2//347/212/266/346/200/201/347/256/241/347/220/206V1/345/220/221V2/350/277/201/347/247/273/344/270/216/346/267/267/347/224/250/346/214/207/345/257/274.md +840 -0
- package/skills/hmos-incremental-ui-align/references/State_Model_Template.md +74 -0
- package/skills/hmos-incremental-ui-align/references/UI_Analysis_Template.md +34 -0
- package/skills/hmos-incremental-ui-align/references/android-to-harmonyOS-ui-atomic-component-mapping-reference.md +2533 -0
- package/skills/hmos-incremental-ui-align/references/android-to-harmonyOS-ui-interaction-mapping-reference.md +555 -0
- package/skills/hmos-incremental-ui-align/references/android-to-harmonyOS-ui-layout-mapping-reference.md +117 -0
- package/skills/hmos-incremental-ui-align/scripts/app_feature_verify.ts +999 -0
- package/skills/hmos-incremental-ui-align/scripts/extract_checklist.ts +343 -0
- package/skills/hmos-incremental-ui-align/scripts/navigation-capure.md +76 -0
- package/skills/hmos-incremental-ui-align/scripts/page_capture.ts +977 -0
- package/skills/hmos-incremental-ui-align/scripts/page_capture_burst.ts +188 -0
|
@@ -0,0 +1,1012 @@
|
|
|
1
|
+
# @ObservedV2装饰器和@Trace装饰器:类属性变化观测
|
|
2
|
+
为了增强状态管理框架对类对象中属性的观测能力,开发者可以使用@ObservedV2装饰器和@Trace装饰器装饰类以及类中的属性。
|
|
3
|
+
@ObservedV2和@Trace提供了对嵌套类对象属性变化直接观测的能力,是状态管理V2中相对核心的能力之一。在阅读本文档前,建议提前阅读: [状态管理概述](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-state-management-overview) 来了解状态管理V2整体的能力架构。
|
|
4
|
+

|
|
5
|
+
@ObservedV2与@Trace装饰器从API version 12开始支持。
|
|
6
|
+
从API version 12开始,@ObservedV2与@Trace装饰器支持在ArkTS卡片中使用。
|
|
7
|
+
从API version 12开始,@ObservedV2与@Trace装饰器支持在元服务中使用。
|
|
8
|
+
#### 概述
|
|
9
|
+
@ObservedV2装饰器与@Trace装饰器用于装饰类以及类中的属性,使得被装饰的类和属性具有深度观测的能力:
|
|
10
|
+
- @ObservedV2装饰器与@Trace装饰器需要配合使用,单独使用@ObservedV2装饰器或@Trace装饰器没有任何作用。
|
|
11
|
+
- 被@Trace装饰器装饰的属性property变化时,仅会通知property关联的组件进行刷新。
|
|
12
|
+
- 在嵌套类中,嵌套类中的属性property被@Trace装饰且嵌套类被@ObservedV2装饰时,才具有触发UI刷新的能力。
|
|
13
|
+
- 在继承类中,父类或子类中的属性property被@Trace装饰且该property所在类被@ObservedV2装饰时,才具有触发UI刷新的能力。
|
|
14
|
+
- 未被@Trace装饰的属性用在UI中无法感知到变化,也无法触发UI刷新。
|
|
15
|
+
- 使用@ObservedV2与@Trace装饰器的类,需通过new操作符实例化后,才具备被观测变化的能力。
|
|
16
|
+
#### 状态管理V1版本对嵌套类对象属性变化直接观测的局限性
|
|
17
|
+
现有状态管理V1版本无法实现对嵌套类对象属性变化的直接观测。
|
|
18
|
+
```
|
|
19
|
+
@Observed
|
|
20
|
+
class Father {
|
|
21
|
+
public son: Son;
|
|
22
|
+
constructor(name: string, age: number) {
|
|
23
|
+
this.son = new Son(name, age);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
@Observed
|
|
27
|
+
class Son {
|
|
28
|
+
public name: string;
|
|
29
|
+
public age: number;
|
|
30
|
+
constructor(name: string, age: number) {
|
|
31
|
+
this.name = name;
|
|
32
|
+
this.age = age;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
@Entry
|
|
36
|
+
@Component
|
|
37
|
+
struct Index {
|
|
38
|
+
@State father: Father = new Father('John', 8);
|
|
39
|
+
build() {
|
|
40
|
+
Row() {
|
|
41
|
+
Column() {
|
|
42
|
+
Text(`name: ${this.father.son.name} age: ${this.father.son.age}`)
|
|
43
|
+
.fontSize(50)
|
|
44
|
+
.fontWeight(FontWeight.Bold)
|
|
45
|
+
.onClick(() => {
|
|
46
|
+
// 嵌套类对象属性变化无法观测
|
|
47
|
+
this.father.son.age++;
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
.width('100%')
|
|
51
|
+
}
|
|
52
|
+
.height('100%')
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
在上述代码中,点击Text组件增加age的值时,不会触发UI刷新。原因在于现有的状态管理框架无法观测到嵌套类中属性age的值变化。V1版本的解决方案是使用 [@ObjectLink装饰器](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-observed-and-objectlink) 与自定义组件来实现观测。
|
|
57
|
+
```
|
|
58
|
+
@Observed
|
|
59
|
+
class Father {
|
|
60
|
+
public son: Son;
|
|
61
|
+
constructor(name: string, age: number) {
|
|
62
|
+
this.son = new Son(name, age);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
@Observed
|
|
66
|
+
class Son {
|
|
67
|
+
public name: string;
|
|
68
|
+
public age: number;
|
|
69
|
+
constructor(name: string, age: number) {
|
|
70
|
+
this.name = name;
|
|
71
|
+
this.age = age;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
@Component
|
|
75
|
+
struct Child {
|
|
76
|
+
// @Observed对象与@ObjectLink一起使用,实现对嵌套类对象属性的观测能力
|
|
77
|
+
@ObjectLink son: Son;
|
|
78
|
+
build() {
|
|
79
|
+
Row() {
|
|
80
|
+
Column() {
|
|
81
|
+
Text(`name: ${this.son.name} age: ${this.son.age}`)
|
|
82
|
+
.fontSize(50)
|
|
83
|
+
.fontWeight(FontWeight.Bold)
|
|
84
|
+
.onClick(() => {
|
|
85
|
+
this.son.age++;
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
.width('100%')
|
|
89
|
+
}
|
|
90
|
+
.height('100%')
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
@Entry
|
|
94
|
+
@Component
|
|
95
|
+
struct Index {
|
|
96
|
+
@State father: Father = new Father('John', 8);
|
|
97
|
+
build() {
|
|
98
|
+
Column() {
|
|
99
|
+
Child({ son: this.father.son })
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
通过这种方式虽然能够实现对嵌套类中属性变化的观测,但是当嵌套层级较深时,代码将会变得十分复杂,易用性差。因此推出类装饰器@ObservedV2与成员变量装饰器@Trace,增强对嵌套类中属性变化的观测能力。
|
|
105
|
+
#### 装饰器说明
|
|
106
|
+
| @ObservedV2类装饰器 | 说明 |
|
|
107
|
+
| --- | --- |
|
|
108
|
+
| 装饰器参数 | 无。 |
|
|
109
|
+
| 类装饰器 | 装饰class。需要放在class的定义前,使用new创建类对象。 |
|
|
110
|
+
| @Trace成员变量装饰器 | 说明 |
|
|
111
|
+
| --- | --- |
|
|
112
|
+
| 装饰器参数 | 无。 |
|
|
113
|
+
| 可装饰的变量 | class中成员属性。属性的类型可以为number、string、boolean、class、[Array](#trace装饰基础类型的数组)、[Date](#trace装饰date类型)、[Map](#trace装饰map类型)、[Set](#trace装饰set类型)等类型。@Trace不支持观察Function类型的数据,修改@Trace装饰的Function类型的数据,UI不会刷新。 |
|
|
114
|
+
#### 观察变化
|
|
115
|
+
使用@ObservedV2装饰的类中被@Trace装饰的属性具有被观测变化的能力,当该属性值变化时,会触发该属性绑定的UI组件刷新。
|
|
116
|
+
- 在嵌套类中使用@Trace装饰的属性具有被观测变化的能力。
|
|
117
|
+
```
|
|
118
|
+
@ObservedV2
|
|
119
|
+
class Son {
|
|
120
|
+
@Trace public age: number = 100;
|
|
121
|
+
}
|
|
122
|
+
class Father {
|
|
123
|
+
public son: Son = new Son();
|
|
124
|
+
}
|
|
125
|
+
@Entry
|
|
126
|
+
@ComponentV2
|
|
127
|
+
struct Index {
|
|
128
|
+
father: Father = new Father();
|
|
129
|
+
build() {
|
|
130
|
+
Column() {
|
|
131
|
+
// 当点击改变age时,Text组件会刷新
|
|
132
|
+
Text(`${this.father.son.age}`)
|
|
133
|
+
.onClick(() => {
|
|
134
|
+
this.father.son.age++;
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
- 在继承类中使用@Trace装饰的属性具有被观测变化的能力。
|
|
141
|
+
```
|
|
142
|
+
@ObservedV2
|
|
143
|
+
class Father {
|
|
144
|
+
@Trace public name: string = 'Tom';
|
|
145
|
+
}
|
|
146
|
+
class Son extends Father {
|
|
147
|
+
}
|
|
148
|
+
@Entry
|
|
149
|
+
@ComponentV2
|
|
150
|
+
struct Index {
|
|
151
|
+
son: Son = new Son();
|
|
152
|
+
build() {
|
|
153
|
+
Column() {
|
|
154
|
+
// 当点击改变name时,Text组件会刷新
|
|
155
|
+
Text(`${this.son.name}`)
|
|
156
|
+
.onClick(() => {
|
|
157
|
+
this.son.name = 'Jack';
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
- 类中使用@Trace装饰的静态属性具有被观测变化的能力。
|
|
164
|
+
```
|
|
165
|
+
@ObservedV2
|
|
166
|
+
class Manager {
|
|
167
|
+
@Trace public static count: number = 1;
|
|
168
|
+
}
|
|
169
|
+
@Entry
|
|
170
|
+
@ComponentV2
|
|
171
|
+
struct Index {
|
|
172
|
+
build() {
|
|
173
|
+
Column() {
|
|
174
|
+
// 当点击改变count时,Text组件会刷新
|
|
175
|
+
Text(`${Manager.count}`)
|
|
176
|
+
.onClick(() => {
|
|
177
|
+
Manager.count++;
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
-
|
|
184
|
+
@Trace装饰内置类型时,可以观测各自API导致的变化:
|
|
185
|
+
| 类型 | 可观测变化的API |
|
|
186
|
+
| --- | --- |
|
|
187
|
+
| Array | push、pop、shift、unshift、splice、copyWithin、fill、reverse、sort |
|
|
188
|
+
| Date | setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds |
|
|
189
|
+
| Map | set, clear, delete |
|
|
190
|
+
| Set | add, clear, delete |
|
|
191
|
+
#### 使用限制
|
|
192
|
+
@ObservedV2与@Trace装饰器存在以下使用限制:
|
|
193
|
+
- 非@Trace装饰的成员属性用在UI上无法触发UI刷新。
|
|
194
|
+
```
|
|
195
|
+
@ObservedV2
|
|
196
|
+
class Person {
|
|
197
|
+
public id: number = 0;
|
|
198
|
+
@Trace public age: number = 8;
|
|
199
|
+
}
|
|
200
|
+
@Entry
|
|
201
|
+
@ComponentV2
|
|
202
|
+
struct Index {
|
|
203
|
+
person: Person = new Person();
|
|
204
|
+
build() {
|
|
205
|
+
Column() {
|
|
206
|
+
// age被@Trace装饰,用在UI中可以触发UI刷新
|
|
207
|
+
Text(`${this.person.age}`)
|
|
208
|
+
.onClick(() => {
|
|
209
|
+
this.person.age++; // 点击会触发UI刷新
|
|
210
|
+
})
|
|
211
|
+
// id未被@Trace装饰,用在UI中不会触发UI刷新
|
|
212
|
+
Text(`${this.person.id}`) // 当id变化时不会刷新
|
|
213
|
+
.onClick(() => {
|
|
214
|
+
this.person.id++; // 点击不会触发UI刷新
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
- @ObservedV2仅能装饰class,无法装饰自定义组件。
|
|
221
|
+
```
|
|
222
|
+
@ObservedV2 // 错误用法,编译时报错
|
|
223
|
+
struct Index {
|
|
224
|
+
build() {
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
- @Trace不能用在没有被@ObservedV2装饰的class上。
|
|
229
|
+
```
|
|
230
|
+
class User {
|
|
231
|
+
id: number = 0;
|
|
232
|
+
@Trace name: string = 'Tom'; // 错误用法,编译时报错
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
- @Trace是class中属性的装饰器,不能用在struct中。
|
|
236
|
+
```
|
|
237
|
+
@ComponentV2
|
|
238
|
+
struct Comp {
|
|
239
|
+
@Trace message: string = 'Hello World'; // 错误用法,编译时报错
|
|
240
|
+
build() {
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
- @ObservedV2、@Trace不能与[@Observed](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-observed-and-objectlink)、[@Track](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-track)混合使用。
|
|
245
|
+
```
|
|
246
|
+
@Observed
|
|
247
|
+
class User {
|
|
248
|
+
@Trace name: string = 'Tom'; // 错误用法,编译时报错
|
|
249
|
+
}
|
|
250
|
+
@ObservedV2
|
|
251
|
+
class Person {
|
|
252
|
+
@Track name: string = 'Jack'; // 错误用法,编译时报错
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
- 使用@ObservedV2与@Trace装饰的类不能和[@State](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-state)等V1的装饰器混合使用,编译时报错。
|
|
256
|
+
```
|
|
257
|
+
// 以@State装饰器为例
|
|
258
|
+
@ObservedV2
|
|
259
|
+
class Job {
|
|
260
|
+
@Trace public jobName: string = 'Teacher';
|
|
261
|
+
}
|
|
262
|
+
@ObservedV2
|
|
263
|
+
class Info {
|
|
264
|
+
@Trace public name: string = 'Tom';
|
|
265
|
+
@Trace public age: number = 25;
|
|
266
|
+
public job: Job = new Job();
|
|
267
|
+
}
|
|
268
|
+
@Entry
|
|
269
|
+
@ComponentV2
|
|
270
|
+
struct Index {
|
|
271
|
+
// @State info: Info = new Info(); 无法混用,编译时报错
|
|
272
|
+
@Local info: Info = new Info();
|
|
273
|
+
build() {
|
|
274
|
+
Column() {
|
|
275
|
+
Text(`name: ${this.info.name}`)
|
|
276
|
+
Text(`age: ${this.info.age}`)
|
|
277
|
+
Text(`jobName: ${this.info.job.jobName}`)
|
|
278
|
+
Button('change age')
|
|
279
|
+
.onClick(() => {
|
|
280
|
+
this.info.age++;
|
|
281
|
+
})
|
|
282
|
+
Button('Change job')
|
|
283
|
+
.onClick(() => {
|
|
284
|
+
this.info.job.jobName = 'Doctor';
|
|
285
|
+
})
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
- 继承自@ObservedV2的类无法和@State等V1的装饰器混用,运行时报错。
|
|
291
|
+
```
|
|
292
|
+
// 以@State装饰器为例
|
|
293
|
+
@ObservedV2
|
|
294
|
+
class Job {
|
|
295
|
+
@Trace public jobName: string = 'Teacher';
|
|
296
|
+
}
|
|
297
|
+
@ObservedV2
|
|
298
|
+
class Info {
|
|
299
|
+
@Trace public name: string = 'Tom';
|
|
300
|
+
@Trace public age: number = 25;
|
|
301
|
+
public job: Job = new Job();
|
|
302
|
+
}
|
|
303
|
+
class Message extends Info {
|
|
304
|
+
constructor() {
|
|
305
|
+
super();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
@Entry
|
|
309
|
+
@Component
|
|
310
|
+
struct Index {
|
|
311
|
+
// @State message: Message = new Message(); 无法混用,运行时报错
|
|
312
|
+
message: Message = new Message();
|
|
313
|
+
build() {
|
|
314
|
+
Column() {
|
|
315
|
+
Text(`name: ${this.message.name}`)
|
|
316
|
+
Text(`age: ${this.message.age}`)
|
|
317
|
+
Text(`jobName: ${this.message.job.jobName}`)
|
|
318
|
+
Button('change age')
|
|
319
|
+
.onClick(() => {
|
|
320
|
+
this.message.age++;
|
|
321
|
+
})
|
|
322
|
+
Button('Change job')
|
|
323
|
+
.onClick(() => {
|
|
324
|
+
this.message.job.jobName = 'Doctor';
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
- 使用@ObservedV2与@Trace装饰器的类,需通过new操作符实例化后,才具备被观测变化的能力。
|
|
331
|
+
- @ObservedV2的类实例无法直接使用JSON.parse反序列化获得(直接使用JSON.parse反序列化获得的对象无法观察属性变化),可搭配三方库[class-transformer](https://gitcode.com/openharmony-tpc/openharmony_tpc_samples/tree/master/class-transformer)实现反序列化后可观察,示例请参考[@ObservedV2装饰对象的序列化与反序列化](#observedv2装饰对象的序列化与反序列化)。
|
|
332
|
+
#### 使用场景
|
|
333
|
+
#### 嵌套类场景
|
|
334
|
+
在下面的嵌套类场景中,Pencil类是Son类中最里层的类,Pencil类被@ObservedV2装饰且属性length被@Trace装饰,此时length的变化能够被观测到。
|
|
335
|
+
@Trace装饰器与现有状态管理框架的 [@Track](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-track) 与 [@State](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-state) 装饰器的能力不同,@Track使class具有属性级更新的能力,但并不具备深度观测的能力;而@State只能观测到对象本身以及第一层的变化,对于多层嵌套场景只能通过封装自定义组件,搭配 [@Observed](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-observed-and-objectlink) 和 [@ObjectLink](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-observed-and-objectlink) 来实现观测。
|
|
336
|
+
- 点击Button('change length'),length是被@Trace装饰的属性,它的变化可以触发关联的UI组件,即UINode (1)的刷新,并输出"id: 1 renderTimes: x"的日志,其中x根据点击次数依次增长。
|
|
337
|
+
- 自定义组件Page中的son是常规变量,因此点击Button('assign Son')并不会观测到变化。
|
|
338
|
+
- 当点击Button('assign Son')后,再点击Button('change length')并不会引起UI刷新。因为此时son的地址改变,其关联的UI组件并没有关联到最新的son。
|
|
339
|
+
```
|
|
340
|
+
import { hilog } from '@kit.PerformanceAnalysisKit';
|
|
341
|
+
const DOMAIN = 0x0001;
|
|
342
|
+
const TAG = 'ArktsObservedV2AndTrace';
|
|
343
|
+
@ObservedV2
|
|
344
|
+
class Pencil {
|
|
345
|
+
@Trace public length: number = 21; // 当length变化时,会刷新关联的组件
|
|
346
|
+
}
|
|
347
|
+
class Bag {
|
|
348
|
+
public width: number = 50;
|
|
349
|
+
public height: number = 60;
|
|
350
|
+
public pencil: Pencil = new Pencil();
|
|
351
|
+
}
|
|
352
|
+
class Son {
|
|
353
|
+
public age: number = 5;
|
|
354
|
+
public school: string = 'some';
|
|
355
|
+
public bag: Bag = new Bag();
|
|
356
|
+
}
|
|
357
|
+
@Entry
|
|
358
|
+
@ComponentV2
|
|
359
|
+
struct Page {
|
|
360
|
+
son: Son = new Son();
|
|
361
|
+
renderTimes: number = 0;
|
|
362
|
+
isRender(id: number): number {
|
|
363
|
+
hilog.info(DOMAIN, TAG, `id: ${id} renderTimes: ${this.renderTimes}`);
|
|
364
|
+
this.renderTimes++;
|
|
365
|
+
return 40;
|
|
366
|
+
}
|
|
367
|
+
build() {
|
|
368
|
+
Column() {
|
|
369
|
+
Text('pencil length' + this.son.bag.pencil.length)
|
|
370
|
+
.fontSize(this.isRender(1)) // UINode (1)
|
|
371
|
+
Button('change length')
|
|
372
|
+
.onClick(() => {
|
|
373
|
+
// 点击更改length值,UINode(1)会刷新
|
|
374
|
+
this.son.bag.pencil.length += 100;
|
|
375
|
+
})
|
|
376
|
+
Button('assign Son')
|
|
377
|
+
.onClick(() => {
|
|
378
|
+
// 由于变量son非状态变量,因此无法刷新UINode(1)
|
|
379
|
+
this.son = new Son();
|
|
380
|
+
})
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
#### 继承类场景
|
|
386
|
+
@Trace支持在类的继承场景中使用,无论是在基类还是继承类中,只有被@Trace装饰的属性才具有被观测变化的能力。
|
|
387
|
+
以下例子中,声明class GrandFather、Father、Uncle、Son、Cousin,继承关系如下图。
|
|
388
|
+

|
|
389
|
+
创建类Son和类Cousin的实例,点击Button('change Son age')和Button('change Cousin age')可以触发UI的刷新。
|
|
390
|
+
```
|
|
391
|
+
import { hilog } from '@kit.PerformanceAnalysisKit';
|
|
392
|
+
const DOMAIN = 0x0001;
|
|
393
|
+
const TAG = 'ArktsObservedV2AndTrace';
|
|
394
|
+
@ObservedV2
|
|
395
|
+
class GrandFather {
|
|
396
|
+
// 被@Trace装饰的属性具有被观测变化的能力
|
|
397
|
+
@Trace public age: number = 0;
|
|
398
|
+
constructor(age: number) {
|
|
399
|
+
this.age = age;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
class Father extends GrandFather {
|
|
403
|
+
constructor(father: number) {
|
|
404
|
+
super(father);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
class Uncle extends GrandFather {
|
|
408
|
+
constructor(uncle: number) {
|
|
409
|
+
super(uncle);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
class Son extends Father {
|
|
413
|
+
constructor(son: number) {
|
|
414
|
+
super(son);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
class Cousin extends Uncle {
|
|
418
|
+
constructor(cousin: number) {
|
|
419
|
+
super(cousin);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
@Entry
|
|
423
|
+
@ComponentV2
|
|
424
|
+
struct Index {
|
|
425
|
+
son: Son = new Son(0);
|
|
426
|
+
cousin: Cousin = new Cousin(0);
|
|
427
|
+
renderTimes: number = 0;
|
|
428
|
+
isRender(id: number): number {
|
|
429
|
+
hilog.info(DOMAIN, TAG, `id: ${id} renderTimes: ${this.renderTimes}`);
|
|
430
|
+
this.renderTimes++;
|
|
431
|
+
return 40;
|
|
432
|
+
}
|
|
433
|
+
build() {
|
|
434
|
+
Row() {
|
|
435
|
+
Column() {
|
|
436
|
+
Text(`Son ${this.son.age}`)
|
|
437
|
+
.fontSize(this.isRender(1))
|
|
438
|
+
.fontWeight(FontWeight.Bold)
|
|
439
|
+
Text(`Cousin ${this.cousin.age}`)
|
|
440
|
+
.fontSize(this.isRender(2))
|
|
441
|
+
.fontWeight(FontWeight.Bold)
|
|
442
|
+
Button('change Son age')
|
|
443
|
+
.onClick(() => {
|
|
444
|
+
this.son.age++;
|
|
445
|
+
})
|
|
446
|
+
Button('change Cousin age')
|
|
447
|
+
.onClick(() => {
|
|
448
|
+
this.cousin.age++;
|
|
449
|
+
})
|
|
450
|
+
}
|
|
451
|
+
.width('100%')
|
|
452
|
+
}
|
|
453
|
+
.height('100%')
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
#### @Trace装饰基础类型的数组
|
|
458
|
+
@Trace装饰数组时,使用支持的API能够观测到变化。支持的API见 [观察变化](#观察变化) 。
|
|
459
|
+
在下面的示例中@ObservedV2装饰的Arr类中的属性numberArr是@Trace装饰的数组,当使用数组API操作numberArr时,可以观测到对应的变化。注意使用数组长度进行判断以防越界访问。
|
|
460
|
+
```
|
|
461
|
+
let nextId: number = 0;
|
|
462
|
+
@ObservedV2
|
|
463
|
+
class Arr {
|
|
464
|
+
public id: number = 0;
|
|
465
|
+
@Trace public numberArr: number[] = [];
|
|
466
|
+
constructor() {
|
|
467
|
+
this.id = nextId++;
|
|
468
|
+
this.numberArr = [0, 1, 2];
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
@Entry
|
|
472
|
+
@ComponentV2
|
|
473
|
+
struct Index {
|
|
474
|
+
arr: Arr = new Arr();
|
|
475
|
+
build() {
|
|
476
|
+
Column() {
|
|
477
|
+
Text(`length: ${this.arr.numberArr.length}`)
|
|
478
|
+
.fontSize(40)
|
|
479
|
+
Divider()
|
|
480
|
+
if (this.arr.numberArr.length >= 3) {
|
|
481
|
+
Text(`${this.arr.numberArr[0]}`)
|
|
482
|
+
.fontSize(40)
|
|
483
|
+
.onClick(() => {
|
|
484
|
+
this.arr.numberArr[0]++;
|
|
485
|
+
})
|
|
486
|
+
Text(`${this.arr.numberArr[1]}`)
|
|
487
|
+
.fontSize(40)
|
|
488
|
+
.onClick(() => {
|
|
489
|
+
this.arr.numberArr[1]++;
|
|
490
|
+
})
|
|
491
|
+
Text(`${this.arr.numberArr[2]}`)
|
|
492
|
+
.fontSize(40)
|
|
493
|
+
.onClick(() => {
|
|
494
|
+
this.arr.numberArr[2]++;
|
|
495
|
+
})
|
|
496
|
+
}
|
|
497
|
+
Divider()
|
|
498
|
+
ForEach(this.arr.numberArr, (item: number, index: number) => {
|
|
499
|
+
Text(`${index} ${item}`)
|
|
500
|
+
.fontSize(40)
|
|
501
|
+
})
|
|
502
|
+
// numberArr是@Trace装饰的数组
|
|
503
|
+
// 使用数组API操作numberArr时,可以观测到对应的变化
|
|
504
|
+
Button('push')
|
|
505
|
+
.onClick(() => {
|
|
506
|
+
this.arr.numberArr.push(50);
|
|
507
|
+
})
|
|
508
|
+
Button('pop')
|
|
509
|
+
.onClick(() => {
|
|
510
|
+
this.arr.numberArr.pop();
|
|
511
|
+
})
|
|
512
|
+
Button('shift')
|
|
513
|
+
.onClick(() => {
|
|
514
|
+
this.arr.numberArr.shift();
|
|
515
|
+
})
|
|
516
|
+
Button('splice')
|
|
517
|
+
.onClick(() => {
|
|
518
|
+
this.arr.numberArr.splice(1, 0, 60);
|
|
519
|
+
})
|
|
520
|
+
Button('unshift')
|
|
521
|
+
.onClick(() => {
|
|
522
|
+
this.arr.numberArr.unshift(100);
|
|
523
|
+
})
|
|
524
|
+
Button('copywithin')
|
|
525
|
+
.onClick(() => {
|
|
526
|
+
this.arr.numberArr.copyWithin(0, 1, 2);
|
|
527
|
+
})
|
|
528
|
+
Button('fill')
|
|
529
|
+
.onClick(() => {
|
|
530
|
+
this.arr.numberArr.fill(0, 2, 4);
|
|
531
|
+
})
|
|
532
|
+
Button('reverse')
|
|
533
|
+
.onClick(() => {
|
|
534
|
+
this.arr.numberArr.reverse();
|
|
535
|
+
})
|
|
536
|
+
Button('sort')
|
|
537
|
+
.onClick(() => {
|
|
538
|
+
this.arr.numberArr.sort();
|
|
539
|
+
})
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
```
|
|
544
|
+
#### @Trace装饰对象数组
|
|
545
|
+
- @Trace装饰对象数组personList以及Person类中的age属性,因此当personList、age改变时均可以观测到变化。
|
|
546
|
+
- 点击Text组件更改age时,Text组件会刷新。
|
|
547
|
+
```
|
|
548
|
+
let nextId: number = 0;
|
|
549
|
+
@ObservedV2
|
|
550
|
+
class Person {
|
|
551
|
+
// @Trace装饰Person类中的age属性,使age可以被观测
|
|
552
|
+
@Trace public age: number = 0;
|
|
553
|
+
constructor(age: number) {
|
|
554
|
+
this.age = age;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
@ObservedV2
|
|
558
|
+
class Info {
|
|
559
|
+
public id: number = 0;
|
|
560
|
+
@Trace public personList: Person[] = [];
|
|
561
|
+
constructor() {
|
|
562
|
+
this.id = nextId++;
|
|
563
|
+
this.personList = [new Person(0), new Person(1), new Person(2)];
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
@Entry
|
|
567
|
+
@ComponentV2
|
|
568
|
+
struct Index {
|
|
569
|
+
info: Info = new Info();
|
|
570
|
+
build() {
|
|
571
|
+
Column() {
|
|
572
|
+
Text(`length: ${this.info.personList.length}`)
|
|
573
|
+
.fontSize(40)
|
|
574
|
+
Divider()
|
|
575
|
+
if (this.info.personList.length >= 3) {
|
|
576
|
+
Text(`${this.info.personList[0].age}`)
|
|
577
|
+
.fontSize(40)
|
|
578
|
+
.onClick(() => {
|
|
579
|
+
this.info.personList[0].age++;
|
|
580
|
+
})
|
|
581
|
+
Text(`${this.info.personList[1].age}`)
|
|
582
|
+
.fontSize(40)
|
|
583
|
+
.onClick(() => {
|
|
584
|
+
this.info.personList[1].age++;
|
|
585
|
+
})
|
|
586
|
+
Text(`${this.info.personList[2].age}`)
|
|
587
|
+
.fontSize(40)
|
|
588
|
+
.onClick(() => {
|
|
589
|
+
this.info.personList[2].age++;
|
|
590
|
+
})
|
|
591
|
+
}
|
|
592
|
+
Divider()
|
|
593
|
+
ForEach(this.info.personList, (item: Person, index: number) => {
|
|
594
|
+
Text(`${index} ${item.age}`)
|
|
595
|
+
.fontSize(40)
|
|
596
|
+
})
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
```
|
|
601
|
+
#### @Trace装饰Map类型
|
|
602
|
+
- 被@Trace装饰的Map类型属性可以观测到调用API带来的变化,包括 set、clear、delete。
|
|
603
|
+
- 因为Info类被@ObservedV2装饰且属性memberMap被@Trace装饰,点击Button('init map')对memberMap赋值也可以观测到变化。
|
|
604
|
+
```
|
|
605
|
+
@ObservedV2
|
|
606
|
+
class Info {
|
|
607
|
+
@Trace public memberMap: Map<number, string> = new Map([[0, 'a'], [1, 'b'], [3, 'c']]);
|
|
608
|
+
}
|
|
609
|
+
@Entry
|
|
610
|
+
@ComponentV2
|
|
611
|
+
struct MapSample {
|
|
612
|
+
info: Info = new Info();
|
|
613
|
+
build() {
|
|
614
|
+
Row() {
|
|
615
|
+
Column() {
|
|
616
|
+
ForEach(Array.from(this.info.memberMap.entries()), (item: [number, string]) => {
|
|
617
|
+
Text(`${item[0]}`)
|
|
618
|
+
.fontSize(30)
|
|
619
|
+
Text(`${item[1]}`)
|
|
620
|
+
.fontSize(30)
|
|
621
|
+
Divider()
|
|
622
|
+
})
|
|
623
|
+
// 被@Trace装饰的Map类型属性可以观测到调用API带来的变化
|
|
624
|
+
Button('init map')
|
|
625
|
+
.onClick(() => {
|
|
626
|
+
this.info.memberMap = new Map([[0, 'a'], [1, 'b'], [3, 'c']]);
|
|
627
|
+
})
|
|
628
|
+
Button('set new one')
|
|
629
|
+
.onClick(() => {
|
|
630
|
+
this.info.memberMap.set(4, 'd');
|
|
631
|
+
})
|
|
632
|
+
Button('clear')
|
|
633
|
+
.onClick(() => {
|
|
634
|
+
this.info.memberMap.clear();
|
|
635
|
+
})
|
|
636
|
+
Button('set the key: 0')
|
|
637
|
+
.onClick(() => {
|
|
638
|
+
this.info.memberMap.set(0, 'aa');
|
|
639
|
+
})
|
|
640
|
+
Button('delete the first one')
|
|
641
|
+
.onClick(() => {
|
|
642
|
+
this.info.memberMap.delete(0);
|
|
643
|
+
})
|
|
644
|
+
}
|
|
645
|
+
.width('100%')
|
|
646
|
+
}
|
|
647
|
+
.height('100%')
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
```
|
|
651
|
+
#### @Trace装饰Set类型
|
|
652
|
+
- 被@Trace装饰的Set类型属性可以观测到调用API带来的变化,包括 add、clear和delete。
|
|
653
|
+
- 因为Info类被@ObservedV2装饰且属性memberSet被@Trace装饰,点击Button('init set')对memberSet赋值也可以观测到变化。
|
|
654
|
+
```
|
|
655
|
+
@ObservedV2
|
|
656
|
+
class Info {
|
|
657
|
+
@Trace public memberSet: Set<number> = new Set([0, 1, 2, 3, 4]);
|
|
658
|
+
}
|
|
659
|
+
@Entry
|
|
660
|
+
@ComponentV2
|
|
661
|
+
struct SetSample {
|
|
662
|
+
info: Info = new Info();
|
|
663
|
+
build() {
|
|
664
|
+
Row() {
|
|
665
|
+
Column() {
|
|
666
|
+
ForEach(Array.from(this.info.memberSet.entries()), (item: [number, number]) => {
|
|
667
|
+
Text(`${item[0]}`)
|
|
668
|
+
.fontSize(30)
|
|
669
|
+
Divider()
|
|
670
|
+
})
|
|
671
|
+
// 被@Trace装饰的Set类型属性可以观测到调用API带来的变化
|
|
672
|
+
Button('init set')
|
|
673
|
+
.onClick(() => {
|
|
674
|
+
this.info.memberSet = new Set([0, 1, 2, 3, 4]);
|
|
675
|
+
})
|
|
676
|
+
Button('set new one')
|
|
677
|
+
.onClick(() => {
|
|
678
|
+
this.info.memberSet.add(5);
|
|
679
|
+
})
|
|
680
|
+
Button('clear')
|
|
681
|
+
.onClick(() => {
|
|
682
|
+
this.info.memberSet.clear();
|
|
683
|
+
})
|
|
684
|
+
Button('delete the first one')
|
|
685
|
+
.onClick(() => {
|
|
686
|
+
this.info.memberSet.delete(0);
|
|
687
|
+
})
|
|
688
|
+
}
|
|
689
|
+
.width('100%')
|
|
690
|
+
}
|
|
691
|
+
.height('100%')
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
```
|
|
695
|
+
#### @Trace装饰Date类型
|
|
696
|
+
- @Trace装饰的Date类型属性可以观测调用API带来的变化,包括 setFullYear、setMonth、setDate、setHours、setMinutes、setSeconds、setMilliseconds、setTime、setUTCFullYear、setUTCMonth、setUTCDate、setUTCHours、setUTCMinutes、setUTCSeconds、setUTCMilliseconds。
|
|
697
|
+
- 因为Info类被@ObservedV2装饰且属性selectedDate被@Trace装饰,点击Button('set selectedDate to 2023-07-08')对selectedDate赋值也可以观测到变化。
|
|
698
|
+
```
|
|
699
|
+
@ObservedV2
|
|
700
|
+
class Info {
|
|
701
|
+
@Trace public selectedDate: Date = new Date('2021-08-08');
|
|
702
|
+
}
|
|
703
|
+
@Entry
|
|
704
|
+
@ComponentV2
|
|
705
|
+
struct DateSample {
|
|
706
|
+
info: Info = new Info();
|
|
707
|
+
build() {
|
|
708
|
+
Column() {
|
|
709
|
+
// @Trace装饰的Date类型属性可以观测调用API带来的变化
|
|
710
|
+
Button('set selectedDate to 2023-07-08')
|
|
711
|
+
.margin(10)
|
|
712
|
+
.onClick(() => {
|
|
713
|
+
this.info.selectedDate = new Date('2023-07-08');
|
|
714
|
+
})
|
|
715
|
+
Button('increase the year by 1')
|
|
716
|
+
.margin(10)
|
|
717
|
+
.onClick(() => {
|
|
718
|
+
this.info.selectedDate.setFullYear(this.info.selectedDate.getFullYear() + 1);
|
|
719
|
+
})
|
|
720
|
+
Button('increase the month by 1')
|
|
721
|
+
.margin(10)
|
|
722
|
+
.onClick(() => {
|
|
723
|
+
this.info.selectedDate.setMonth(this.info.selectedDate.getMonth() + 1);
|
|
724
|
+
})
|
|
725
|
+
Button('increase the day by 1')
|
|
726
|
+
.margin(10)
|
|
727
|
+
.onClick(() => {
|
|
728
|
+
this.info.selectedDate.setDate(this.info.selectedDate.getDate() + 1);
|
|
729
|
+
})
|
|
730
|
+
DatePicker({
|
|
731
|
+
start: new Date('1970-1-1'),
|
|
732
|
+
end: new Date('2100-1-1'),
|
|
733
|
+
selected: this.info.selectedDate
|
|
734
|
+
})
|
|
735
|
+
}.width('100%')
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
```
|
|
739
|
+
#### 常见问题
|
|
740
|
+
#### @ObservedV2装饰对象的序列化与反序列化
|
|
741
|
+
@ObservedV2装饰的对象序列化后会为@Trace装饰的属性添加__ob_前缀。
|
|
742
|
+
```
|
|
743
|
+
@ObservedV2
|
|
744
|
+
class Info {
|
|
745
|
+
@Trace name: string = 'Tom';
|
|
746
|
+
@Trace age: number = 24;
|
|
747
|
+
}
|
|
748
|
+
let realInfo: Info = new Info();
|
|
749
|
+
let jsonResult: string = JSON.stringify(realInfo); // '{"__ob_name":"Tom","__ob_age":24}'
|
|
750
|
+
```
|
|
751
|
+
将@ObservedV2装饰的对象通过JSON.stringify序列化后,再通过JSON.parse反序列化,将失去观察能力。
|
|
752
|
+
```
|
|
753
|
+
@ObservedV2
|
|
754
|
+
class Info {
|
|
755
|
+
@Trace name: string = 'Tom';
|
|
756
|
+
@Trace age: number = 24;
|
|
757
|
+
}
|
|
758
|
+
let realInfo: Info = new Info();
|
|
759
|
+
let jsonResult: string = JSON.stringify(realInfo); // '{"__ob_name":"Tom","__ob_age":24}'
|
|
760
|
+
let parseInfo: Info = JSON.parse(jsonResult);
|
|
761
|
+
// 与直接通过new操作符创建的对象不同,JSON.parse获得的对象实际并不是Info的实例,所以无属性观察能力
|
|
762
|
+
let isInfoByNew: boolean = realInfo instanceof Info; // true
|
|
763
|
+
let isInfoByParse: boolean = parseInfo instanceof Info; // false
|
|
764
|
+
```
|
|
765
|
+
可以配合三方库 [class-transformer](https://gitcode.com/openharmony-tpc/openharmony_tpc_samples/tree/master/class-transformer) 实现反序列化后可观察。
|
|
766
|
+
class-transformer可以通过如下命令安装。
|
|
767
|
+
```
|
|
768
|
+
ohpm install class-transformer
|
|
769
|
+
```
|
|
770
|
+
```
|
|
771
|
+
import { plainToInstance } from 'class-transformer'; // 导入三方库
|
|
772
|
+
@ObservedV2
|
|
773
|
+
class Info {
|
|
774
|
+
@Trace name: string = 'Tom';
|
|
775
|
+
@Trace age: number = 24;
|
|
776
|
+
}
|
|
777
|
+
let realInfo: Info = new Info();
|
|
778
|
+
let jsonResult: string = JSON.stringify(realInfo); // '{"__ob_name":"Tom","__ob_age":24}'
|
|
779
|
+
let parseInfo: Info = JSON.parse(jsonResult);
|
|
780
|
+
let transformedInfo: Info = plainToInstance(Info, parseInfo);
|
|
781
|
+
let isInfoByTransformed: boolean = transformedInfo instanceof Info; // true
|
|
782
|
+
```
|
|
783
|
+
若为多层对象嵌套场景,需要进行额外处理,包括:
|
|
784
|
+
- 去除序列化结果中的__ob_前缀,否则内层对象无法被正确转换。
|
|
785
|
+
- 使用class-transformer库中提供的@Type装饰器(为与状态管理V2的[@Type装饰器](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-new-type)区分,示例中重命名为TypeFromLibrary)标记里层对象的类型。
|
|
786
|
+
使用三方库的@Type装饰器需要安装 [reflect-metadata](https://gitcode.com/openharmony-tpc/openharmony_tpc_samples/tree/master/reflect-metadata) 。
|
|
787
|
+
reflect-metadata可以通过如下命令安装。
|
|
788
|
+
```
|
|
789
|
+
ohpm install reflect-metadata@0.2.1
|
|
790
|
+
```
|
|
791
|
+
```
|
|
792
|
+
import { plainToInstance, Type as TypeFromLibrary} from 'class-transformer'; // 导入三方库
|
|
793
|
+
import 'reflect-metadata'; // 三方库的@Type装饰器需要使用
|
|
794
|
+
@ObservedV2
|
|
795
|
+
class Info {
|
|
796
|
+
@Trace name: string = 'Tom';
|
|
797
|
+
@Trace age: number = 24;
|
|
798
|
+
}
|
|
799
|
+
@ObservedV2
|
|
800
|
+
class InfoWrapper {
|
|
801
|
+
// 使用三方库的@Type装饰器(重命名为TypeFromLibrary)标记内层属性的类型
|
|
802
|
+
@TypeFromLibrary(() => Info)
|
|
803
|
+
@Trace info: Info = new Info();
|
|
804
|
+
}
|
|
805
|
+
let realWrapper: InfoWrapper = new InfoWrapper();
|
|
806
|
+
let infoWrapperJson: string = JSON.stringify(realWrapper); // '{"__ob_info":{"__ob_name":"Tom","__ob_age":24}}'
|
|
807
|
+
// 去除属性key的'__ob_'前缀,此处仅做演示,开发者需根据实际类型定义情况完成去除key中的'__ob_'前缀
|
|
808
|
+
let jsonHandled = infoWrapperJson.replaceAll('__ob_', ''); // '{"info":{"name":"Tom","age":24}}'
|
|
809
|
+
let wrapperHandled = plainToInstance(InfoWrapper, JSON.parse(jsonHandled));
|
|
810
|
+
let isWrapper: boolean = wrapperHandled instanceof InfoWrapper; // true
|
|
811
|
+
let isInfo: boolean = (wrapperHandled.info) instanceof Info; // true
|
|
812
|
+
```
|
|
813
|
+
在UI中使用的完整示例如下。
|
|
814
|
+
```
|
|
815
|
+
import { plainToInstance, Type as TypeFromLibrary } from 'class-transformer'; // 导入三方库
|
|
816
|
+
import 'reflect-metadata'; // 三方库的@Type装饰器需要使用
|
|
817
|
+
// 模拟json键值对对象
|
|
818
|
+
let testJSON: Record<string, ESObject> = {
|
|
819
|
+
'id': 1,
|
|
820
|
+
'info': {
|
|
821
|
+
'name': 'Tom',
|
|
822
|
+
'age': 24
|
|
823
|
+
},
|
|
824
|
+
'friends': [
|
|
825
|
+
{
|
|
826
|
+
'name': 'John',
|
|
827
|
+
'age': 23
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
'name': 'Mary',
|
|
831
|
+
'age': 24
|
|
832
|
+
}
|
|
833
|
+
]
|
|
834
|
+
}
|
|
835
|
+
@ObservedV2
|
|
836
|
+
class Info {
|
|
837
|
+
@Trace public name?: string;
|
|
838
|
+
@Trace public age?: number;
|
|
839
|
+
}
|
|
840
|
+
@ObservedV2
|
|
841
|
+
class Person {
|
|
842
|
+
public id?: number;
|
|
843
|
+
// 使用三方库的@Type装饰器(重命名为TypeFromLibrary)标记内层属性的类型
|
|
844
|
+
@TypeFromLibrary(() => Info)
|
|
845
|
+
@Trace public info?: Info;
|
|
846
|
+
// 使用三方库的@Type装饰器(重命名为TypeFromLibrary)标记内层属性的类型
|
|
847
|
+
@TypeFromLibrary(() => Info)
|
|
848
|
+
@Trace public friends?: Info[];
|
|
849
|
+
}
|
|
850
|
+
@Entry
|
|
851
|
+
@ComponentV2
|
|
852
|
+
struct SerializationAndDeserialization {
|
|
853
|
+
@Local person: Person | undefined = undefined;
|
|
854
|
+
aboutToAppear(): void {
|
|
855
|
+
this.person = plainToInstance(Person, testJSON); // 直接将对象通过plainToInstance转为Person实例
|
|
856
|
+
}
|
|
857
|
+
build() {
|
|
858
|
+
Column() {
|
|
859
|
+
Text(`name: ${this.person?.info?.name}, age: ${this.person?.info?.age}`)
|
|
860
|
+
.onClick(() => {
|
|
861
|
+
if (this.person?.info?.age) {
|
|
862
|
+
this.person!.info!.age++; // 修改可观察
|
|
863
|
+
}
|
|
864
|
+
})
|
|
865
|
+
ForEach(this.person?.friends, (item: Info) => {
|
|
866
|
+
Text(`friend name: ${item.name}, age: ${item.age}`)
|
|
867
|
+
.onClick(() => {
|
|
868
|
+
if (item.age) {
|
|
869
|
+
item.age++; // 修改可观察
|
|
870
|
+
}
|
|
871
|
+
})
|
|
872
|
+
})
|
|
873
|
+
Button('Refresh Info')
|
|
874
|
+
.onClick(() => {
|
|
875
|
+
let json: string =
|
|
876
|
+
`{
|
|
877
|
+
"id":12,
|
|
878
|
+
"__ob_info":
|
|
879
|
+
{
|
|
880
|
+
"__ob_name":"Jimmy",
|
|
881
|
+
"__ob_age":35
|
|
882
|
+
},
|
|
883
|
+
"__ob_friends":[
|
|
884
|
+
{
|
|
885
|
+
"__ob_name":"Bob",
|
|
886
|
+
"__ob_age":30
|
|
887
|
+
},
|
|
888
|
+
{
|
|
889
|
+
"__ob_name":"Kevin",
|
|
890
|
+
"__ob_age":33
|
|
891
|
+
}
|
|
892
|
+
]
|
|
893
|
+
}`;
|
|
894
|
+
// 去除'__ob_'前缀后通过JSON.parse与plainToInstance将json字符串转化成Person对象
|
|
895
|
+
this.person = plainToInstance(Person, JSON.parse(json.replaceAll('__ob_', '')));
|
|
896
|
+
})
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
```
|
|
901
|
+
#### router传递的@ObservedV2类型显示异常
|
|
902
|
+
用router传递的@ObservedV2类,由于经过序列化生成的属性名称与类中的原始属性名称不一致,不能直接通过as类型转换成@ObservedV2的实例,需要反序列化重新生成@ObservedV2实例。反序列化相关内容请参考 [@ObservedV2装饰对象的序列化与反序列化](#observedv2装饰对象的序列化与反序列化) 。
|
|
903
|
+
【反例】
|
|
904
|
+
```
|
|
905
|
+
// 文件pages/faqs/RouterIndex.ets内容
|
|
906
|
+
@ObservedV2
|
|
907
|
+
export class RouterModel {
|
|
908
|
+
@Trace id: number = -1;
|
|
909
|
+
@Trace info: string = 'default';
|
|
910
|
+
}
|
|
911
|
+
@Entry
|
|
912
|
+
@ComponentV2
|
|
913
|
+
struct RouterIndex {
|
|
914
|
+
@Local paramsInfo: RouterModel = new RouterModel();
|
|
915
|
+
onJumpClick(): void {
|
|
916
|
+
this.paramsInfo.id = 0;
|
|
917
|
+
this.paramsInfo.info = 'RouterModel';
|
|
918
|
+
this.getUIContext().getRouter().pushUrl({
|
|
919
|
+
url: 'pages/faqs/ChildPage',
|
|
920
|
+
params: this.paramsInfo // 传递@ObservedV2实例到子页面
|
|
921
|
+
}, (err) => {
|
|
922
|
+
if (err) {
|
|
923
|
+
console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`);
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
console.info('Invoke pushUrl succeeded.');
|
|
927
|
+
})
|
|
928
|
+
}
|
|
929
|
+
build() {
|
|
930
|
+
Column() {
|
|
931
|
+
Text('Parent page')
|
|
932
|
+
Button('Jump')
|
|
933
|
+
.onClick(() => {
|
|
934
|
+
this.onJumpClick();
|
|
935
|
+
})
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
```
|
|
940
|
+
```
|
|
941
|
+
// 文件pages/faqs/ChildPage.ets内容
|
|
942
|
+
import { RouterModel } from './RouterIndex';
|
|
943
|
+
@Entry
|
|
944
|
+
@ComponentV2
|
|
945
|
+
struct Detail {
|
|
946
|
+
@Local params?: RouterModel
|
|
947
|
+
aboutToAppear(): void {
|
|
948
|
+
// 错误使用方式!@ObservedV2类型通过router传递无法直接类型转换
|
|
949
|
+
this.params = this.getUIContext().getRouter().getParams() as RouterModel;
|
|
950
|
+
}
|
|
951
|
+
build() {
|
|
952
|
+
Column() {
|
|
953
|
+
Text(`Detail Page: ${this.params?.id} ${this.params?.info}`) // 由于传递数据失败,这里会显示undefined
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
```
|
|
958
|
+
【正例】
|
|
959
|
+
```
|
|
960
|
+
@ObservedV2
|
|
961
|
+
export class RouterModel {
|
|
962
|
+
@Trace public id: number = -1;
|
|
963
|
+
@Trace public info: string = 'default';
|
|
964
|
+
}
|
|
965
|
+
@Entry
|
|
966
|
+
@ComponentV2
|
|
967
|
+
struct RouterIndex {
|
|
968
|
+
@Local paramsInfo: RouterModel = new RouterModel();
|
|
969
|
+
onJumpClick(): void {
|
|
970
|
+
this.paramsInfo.id = 0;
|
|
971
|
+
this.paramsInfo.info = 'RouterModel';
|
|
972
|
+
this.getUIContext().getRouter().pushUrl({
|
|
973
|
+
url: 'pages/faqs/ChildPage',
|
|
974
|
+
params: this.paramsInfo // 传递@ObservedV2实例到子页面
|
|
975
|
+
}, (err) => {
|
|
976
|
+
if (err) {
|
|
977
|
+
console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`);
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
console.info('Invoke pushUrl succeeded.');
|
|
981
|
+
})
|
|
982
|
+
}
|
|
983
|
+
build() {
|
|
984
|
+
Column() {
|
|
985
|
+
Text('Parent page')
|
|
986
|
+
Button('Jump')
|
|
987
|
+
.onClick(() => {
|
|
988
|
+
this.onJumpClick();
|
|
989
|
+
})
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
```
|
|
994
|
+
```
|
|
995
|
+
import { RouterModel } from './RouterIndex';
|
|
996
|
+
import { plainToInstance } from 'class-transformer'; // 导入三方库
|
|
997
|
+
@Entry
|
|
998
|
+
@ComponentV2
|
|
999
|
+
struct Detail {
|
|
1000
|
+
@Local params?: RouterModel
|
|
1001
|
+
aboutToAppear(): void {
|
|
1002
|
+
this.params =
|
|
1003
|
+
plainToInstance(RouterModel, JSON.parse(JSON.stringify(this.getUIContext().getRouter().getParams())));
|
|
1004
|
+
}
|
|
1005
|
+
build() {
|
|
1006
|
+
Column() {
|
|
1007
|
+
Text(`Detail Page: ${this.params?.id} ${this.params?.info}`)
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
```
|
|
1012
|
+

|