ArkUI -- 管理数据对象的状态 (状态管理 V2)

@ObservedV2 和 @Trace: 类属性变化观测
@ObservedV2装饰器和@Trace装饰器:类属性变化观测
状态管理V1 版本无法实现对嵌套类对象属性变化的直接观察,通常需要使用 @ObjectLink 装饰器和自定义组件来实现观察,但当嵌套层级较深时,代码会变得是否复杂,易用性差。
@ObservedV2 和 @Trace 提供了对嵌套类对象属性变化的直接观察能力。
| 装饰器 | 装饰器参数 | 装饰的类型 |
|---|---|---|
| @ObservedV2 | 无 | class |
| @Trace | 无 | class 中成员属性,属性的类型可为基本数据类型和 Array、Date、Map、Set 等,不支持 Function 类型 |
@ObservedV2 用于装饰类,@Trace 用于装饰类中的属性,使得被装饰的类和属性具有深度观测的能力:
- @ObservedV2 和 @Trace 需配合使用,单独使用 @ObservedV2 或 @Trace 没有任何作用
- 被 @Trace 装饰的属性的值变化时,仅会通知与其关联的组件进行刷新;未被 @Trace 装饰的属性值的变化无法触发 UI 刷新
- 使用 @ObservedV2 和 @Trace 装饰的类,需通过 new 操作实例化后,才具备被观测变化的能力
- 在继承类中,父类或子类中的属性被 @Trace 装饰,且该属性所在的类被 @ObservedV2 装饰是,才具有 UI 刷新的能力
- 在嵌套类中,嵌套类中的属性被 @Trace 装饰且嵌套类被 @Observed 装饰时,才具有触发 UI 刷新的能力
- @Trace 装饰 Array、Date、Map、Set 的内置类型时,可观测到集合内置类的变化
ObservedV2/@Trace 有自己独立的观测能力,不仅可在 @ComponentV2 中使用,也可以独立在 @Component 中使用,但不可与 V1 状态管理装饰器混合使用
在 @ComponentV2 自定义组件中,被 @ObservedV2 与 @Trace 装饰的变量具有深度观察能力,但对对象整体赋值时,UI 不会自动刷新,状态变量对象需被 @Local 装饰,才可观测对象本身的变化
使用限制
- @Trace 是 class 中属性的装饰器,不能用在 struct 中
- @ObservedV2 仅能装饰 class,无法装饰自定义组件
- 使用 @ObservedV2 与 @Trace 装饰的类不能和 @State 等V1的装饰器混合使用,编译时报错,需用 @Local 等V2的装饰器
- 继承自 @ObservedV2 的类无法和 @State 等V1的装饰器混合使用,运行时报错
- @ObservedV2 的类实例无法直接使用 JSON.parse 反序列化获得 (直接使用 JSON.parse 反序列化获得的对象无法观察属性变化,需使用
makeObserved方法转换)
使用场景
嵌套类场景
// 嵌套类必须被 @ObservedV2 和 @Trace 装饰,才能触发 UI
@ObservedV2
class User {
@Trace age: number = 10;
}
// 外层的类可以不被 @ObservedV2 和 @Trace 装饰
class Login{
user: User = new User();
}
@Entry
@ComponentV2
export struct MainPage {
// login 的嵌套类 User 被 @ObserveV2 装饰,不可用 @State 等 V1 的装饰器
// 即使 login 没有被 @Local 装饰,其 age 属性的改变也会触发 UI 刷新
@Local login: Login = new Login();
build() {
Column({space: 20}) {
Button('change age: ' + this.login.user.age).onClick(()=> {
this.login.user.age++
})
}.width('100%')
}
}
继承类场景
@Trace 支持在类的继承场景中使用,无论是在基类还是继承类中,只有被 @Trace 装饰的属性才具有被观测变化的能力。
@ObservedV2
class User {
// 被 @Trace 装饰,值的变化可触发 UI 刷新
@Trace age: number = 10;
}
@ObservedV2
class Vip extends User {
// 被 @Trace 装饰,值的变化可触发 UI 刷新
@Trace level: number = 1;
// 未被 @Trace 装饰,不会触发 UI 刷新
date: string = '';
}
@Entry
@ComponentV2
export struct MainPage {
@Local vip: Vip = new Vip();
build() {
Column({space: 20}) {
Button('change age: ' + this.vip.age).onClick(()=> {
this.vip.age++
})
Button('change level: ' + this.vip.level).onClick(()=> {
this.vip.level ++
})
}.width('100%')
}
}
@Trace 装饰对象数组
@Trace 装饰 Array、Date、Map、Set 的内置类型时,可观测到集合内置类的变化
@ObservedV2
class User {
// 被 @Trace 装饰,值的变化可触发 UI 刷新
@Trace age: number;
constructor(age: number) {
this.age = age;
}
}
@Entry
@ComponentV2
export struct MainPage {
// 数组的 userList 的内置类 User 的 age 属性被 @Trace 装饰,其变化可触发 UI 刷新
userList: Array<User> = [new User(10)];
build() {
Column({space: 20}) {
Button('change age: ' + this.userList[0].age).onClick(()=> {
this.userList[0].age++
})
}.width('100%')
}
}
@ObservedV2装饰对象的序列化与反序列化
@ObservedV2 装饰的对象序列化后会为 @Trace 装饰的属性添加 __ob_ 前缀
@ObservedV2
class Info {
@Trace name: string = 'Tom';
@Trace age: number = 24;
}
let realInfo: Info = new Info();
let jsonResult: string = JSON.stringify(realInfo); // '{"__ob_name":"Tom","__ob_age":24}'
由于 @ObservedV2 装饰的对象序列化后会为 @Trace 装饰的属性添加 __ob_ 前缀,再通过 JSON.parse 对其反序列化,将失去观察能力,且得到的并非原对象实例,不能直接通过 as 强制类型转换成原对象。
@ObservedV2
class Info {
@Trace name: string = 'Tom';
@Trace age: number = 24;
}
let jsonResult: string = JSON.stringify(new Info()); // '{"__ob_name":"Tom","__ob_age":24}'
let parseInfo: Info = JSON.parse(jsonResult); // parseInfo 实际并不是 Info 的实例
let name: string = parseInfo.name; // parseInfo 的属性名已经变为了 __ob_name,parseInfo.name 为 undefined;
在传递 (Router 路由传参、Navigation 组件传参、跨线程通信) 和存储 (Preferences 持久化) 对象时,都会经历 “序列化+反序列化” 的过程
如:通过 router.pushUrl() 参数对象时,参数对象会被序列化后传递,目标页通过 router.getParams() 拿到的是反序列化后重新生成的对象。
// FirstPge 跳转,并传递 @ObservedV2 实例
this.getUIContext().getRouter().pushUrl({
url: 'pages/secondPage',
params: this.info;
})
// secondPage 接收参数
// 错误的方式!
// 通过 router 传递 @ObservedV2 对象,其在获取后属性名已经改变了,拼接了 __ob_ 前缀,无法通过 as 直接强制转换
// this.params 的属性名为 __ob_name,非原始 Info 的实例,不可观测
this.params = this.getUIContext().getRouter().getParams() as Info;
为了解决上述问题,可配合三方库 class-transformer 实现反序列,对象的属性名不变,且可观测。
安装 class-transformer
ohpm install class-transformer
【示例】
import { plainToInstance } from 'class-transformer'; // 导入三方库
@ObservedV2
class Info {
@Trace name: string = 'Assassin';
@Trace age: number = 28;
}
@Entry
@ComponentV2
export struct MainPage {
jsonStr: string = "{\"name\":\"Assassin\",\"age\":28}";
// 通过反序列得到的对象实例 parseInfo,无法观测
@Local parseInfo: Info = JSON.parse(this.jsonStr);
// 三方库 class-transformer 实现的反序列化实例,可以被观测
@Local transformedParseInfo: Info = plainToInstance(Info, this.parseInfo);
// 将 Info 序列化后,再返序列化得到的对象
@Local infoStr: string = JSON.stringify(new Info());
@Local info: Info = JSON.parse(this.infoStr) as Info;
// 三方库 class-transformer 将 info 转为可被观察的对象
@Local transformedInfo: Info = plainToInstance(Info, this.info);
aboutToAppear(): void {
// 接收 router 参数,需先将接收的参数序列化
let paramStr: string = JSON.stringify(this.getUIContext().getRouter().getParams());
// 反序列化后,通过 plainToInstance 转为可被观察的对象
let params: Info = plainToInstance(Info, JSON.parse(paramStr));
}
build() {
Column({space: 20}) {
// {"__ob_name":"Assassin", "__ob_age":28}"
Text(this.infoStr)
// this.info 已经不是 Info 对象了,无法通过 as 强转化,属性名变为 __ob_name
// this.info.name 为 undefined,属性名变为了 __ob_name
Text(`info.name: ${this.info.name}, info.age: ${this.info.age}`)
// 由于定义了 info 的类型为 Info,所以 this.info.__ob_name 编译报错
Text(`info.name: ${JSON.parse(this.infoStr).__ob_name}`)
// this.transformedInfo 是通过 info 转换的,其属性名又转回为 name 和 age 了
// 且通过 plainToInstance 转换得到的对象,能被观测,可触发 UI 刷新
Button('transformedInfo age: ' + this.transformedInfo.age).onClick(()=> {
this.transformedInfo.age ++;
})
// 通过 JSON.parse 得到的对象实例 parseInfo,无法观测
Button('parseInfo age: ' + this.parseInfo.age).onClick(()=> {
this.parseInfo.age ++;
})
// this.transformedParseInfo 是 parseInfo 转换的,能被观测,可触发 UI 刷新
Button('transformedParseInfo age: ' + this.transformedParseInfo.age).onClick(()=> {
this.transformedParseInfo.age ++;
})
}.width('100%')
}
}
若为多层对象嵌套场景,需要进行额外处理:
- 去除序列化结果中的
__ob_前缀,否则内存对象无法被正确转换 - 使用 class-transformer 库中提供的 @Type 装饰器 (为与状态管理 V2 的 @Type 装饰器区分,引用时将其名为为 TypeFormLibrary) 标记里层对象的类型
使用三方库的 @Type 装饰器需安装 reflect-metadata
ohpm install reflect-metadata@0.2.1
【示例】
// 没有 '__ob_'前缀的 JSON 字符串
infoWrapper: InfoWrapper = new InfoWrapper();
// '{"__ob_info":{"__ob_name":"Assassin","__ob_age":28}}'
infoWrapperJson: string = JSON.stringify(this.infoWrapper);
// 去除属性key的'__ob_'前缀
jsonHandled: string = this.infoWrapperJson.replaceAll('__ob_', ''); // '{"info":{"name":"Tom","age":24}}'
// 用无前缀的字符串反序列化,并通过 plainToInstance 转为可观测对象
wrapperHandled: InfoWrapper = plainToInstance(InfoWrapper, JSON.parse(this.jsonHandled));
@Monitor:状态变量修改异步监听
| @Monitor 装饰器 | 说明 |
|---|---|
| 装饰器参数 | 字符串类型的监听对象属性名或状态变量名,可同时监听多个对象属性,用逗号隔开,API 26 开始,新增了可选参数 MonitorDecoratorOptions 配置项 |
| 装饰对象 | 成员方法,@Monitor 监听的属性发生变化时,会触发的回调方法,该回调方法以 IMonitor 类型的变量作为参数 |
@Monitor 只能以字符串、const 常量、enum 枚举值作为参数,若使用变量作为参数,仅会监听 @Monitor 初始化时,变量值所对应的属性,无法随变量的改变动态更新监听的属性
@Monitor 使用规则
@Monitor 装饰器用于监听状态变量的修改
- 当观测的属性变化时,@Monitor 装饰器定义的回调方法将被调用,判断属性是否变化使用的是严格相等 (===);同一事件内,当被观察的属性多次改变时,只会触发一次回调 (初始值和最终值来比较判断属性是否变化)
- @Monitor 装饰器能同时监听多个状态变量或属性,这些变量名直接用 “,” 隔开
- @Monitor 具有深度监听的能力,能监听 @ObservedV2 装饰的嵌套类、对象数组中指定项,以及多维数组的变化
- @Monitor 可以在 @ComponentV2 的自定义组件或 @ObservedV2 装饰的类中使用
- 在 @ComponentV2 中,只有被 @Local、@Param、@Provider、@Consumer、@Computed 装饰的状态变量,才能被 @Monitor 监听
- 在 @ObservedV2 中,@Monitor 监听的属性需被 @Trace 装饰,可监听深层属性的变化
- 当 @Monitor 监听整个数组时,无法监听内置类的变化,只能监听数组的整体赋值,和长度变化 (插入、删除)
- 对继承类,@Monitor 可在继承链中,可对 @Trace 装饰的属性进行多次监听,若属性变化,回调均会被调用
- 应避免在 @Monitor 中再次改变被监听的属性,会导致无限循环
- 不建议在一个类中对同一属性进行多次 @Monitor 的监听,当一个类中存在对一个属性的多次监听时,只有最后一个定义的监听方法会生效
@Monitor 仅会保存变量可访问的值,当状态变量变为不可访问的状态时 (undefined 或 null),并不会记录其值的变化 (API20 开始,若需要监听到可访问与不可访问的状态的切换,可使用
addMonitor)
API 23 开始,增加了对 @Monitor 入参的编译时校验,若入参不符合监听条件 (如:未被 @Trace 装饰),将会有编译告警,但 @Monitor 回调仍会被触发
@Monitor 语法
-
未使用配置项的 @Monitor 语法
@Monitor('path') onValueChange(monitor: IMonitor) { } -
使用配置项的 @Monitor 语法
// 使用配置项,默认支持通配符能力,监听path对象内任意可观察变化 @Monitor({}, 'path.*') onValueChange2(monitor: IMonitor) { } // 使用配置项,显式配置不支持通配符 @Monitor({ enableWildcard: false }, 'path') onValueChanged1(monitor: IMonitor) { } // 使用配置项,显式配置支持通配符 @Monitor({ enableWildcard: true }, 'path.*') onValueChange3(monitor: IMonitor) { }
@Monitor 是否使用配置项的对比
| 场景 | 未使用配置项的 @Monitor | 使用了配置项的 @Monitro |
|---|---|---|
| 使用通配符 | 不支持 | 支持 |
| 监听不可监听的变量 | 存在被连带触发监听的可能 | 忽略不可监听变量,对路径的监听变为互相独立的监听 |
| 变量可访问性变化 | 仅记录变量可访问时的值,变量变为不可访问 (null、undefined) 的状态时,并不会记录其值的变化 | 变量从可访问变为不可访问,或从不可访问变为可访问,均能正常处理 |
未使用配置项的 @Monitor,当监听的变量变为不可访问 (null、undefined) 的状态后,若 new 了新的实例,仍可监听到变化
@Monitor 监听包含通配符的路径
从 API26 开始,@Monitor 的传参支持通配符能力,当使用配置项 MonitorDecoratorOptions 时,将默认开启通配符支持。
通配符可作为路径中的后缀,监听该路径最后确定值中的变化。通配符路径的语法规则为:
- 通配符只能出现在路径末尾
- 一个路径中最多仅可出现一个通配符
通配符路径实例:
| 路径 | 说明 |
|---|---|
| obj.* | obj 为 @ObservedV2 装饰的对象。监听该路径的 @Monitor 可触发回调的情况如下: 1、对 obj 整体赋值 2、obj 任意 @Trace 属性变化 |
| arr.* | arr 为可观察数组。监听该路径的 @Monitor 可触发回调的情况如下: 1、对 arr 整体赋值 2、arr 任意元素变化或数组长度变化 3、调用数组的 API (如:push、pop、sort、fill 等) |
| obj.objA.* | objA 为 @ObservedV2 装饰的嵌套对象。监听该路径的 @Monitor 可触发回调的情况如下: 1、对 obj 整体赋值且 objA 变化 2、对 objA 整体赋值 3、objA 任意 @Trace 属性变化 |
| arr.1.* | arr 为嵌套可观察数组。监听该路径的 @Monitor 可触发回调的情况如下: 1、对 arr 整体赋值且下标为 1 的数组发生变化 2、arr 下标为 1 的数组任意元素变化或数组长度变化 3、调用 arr 下标为 1 的数组的 API |
使用通配符时,IMonitor 的 dirty 数组能正常包含通配符路径,但其对应的 IMonitorValue 的 before 值与 now 值都将为 undefined
@Monitor 与 @Watch 对比
状态管理 V1 的 @Watch 装饰器无法实现度对象、数组中某一属性或数组项变化的监听,且无法获取变化之前的值
| 用法 | @Watch | @Monitor |
|---|---|---|
| 参数 | 回调方法名 | 监听状态变量名、属性名 |
| 装饰对象 | 监听的状态变量 | 成员方法,@Monitor 监听的属性发生变化时,会触发回调方法 |
| 监听对象 | 监听对象为状态变量 | 监听对象为状态变量或 @Trace 装饰的类成员属性 |
| 监听目标数 | 只能监听单个状态变量 | 能同时监听多个状态变量或属性 |
| 使用限制 | 只能在 @Component 自定义组件中使用 | 可在 @ComponentV2 自定义组件和 @ObservedV2 类中使用 |
| 监听能力 | 跟随状态变量的观察能力 (最外层) | 跟随状态变量的观察能力 (@ObservedV2 对象可深度监听) |
| 能否获取变化前的值 | 不能 | 能 |
- V1 状态变量的变化,会触发 @Watch 的同步执行,若状态变量被修改多次,则 @Watch 回调会同步执行多次
- V2 状态变量的变化,会触发 @Monitor 的异步执行,若在一次事件中状态变量被多次修改,@Monitor 回调只会执行一次
监听变化
监听 @ComponentV2 中的状态变量
- @ComponentV2 中,@Monitor 监听的状态变量需被 @Local、@Param、@Provider、@Consumer、@Computed 装饰
- @Monitor 监听的状态变量为类对象时,仅能监听对象整体的变化 (重新 new 创建的实例),若要监听类属性的变化需要类属性被 @Trace 装饰
- 监听属性时,@Monitor 的入参为,状态变量.属性 (如: info.age)
@ObservedV2
class Info {
name: string;
@Trace age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
@Entry
@ComponentV2
export struct MainPage {
@Local info: Info = new Info('assassin', 20);
// 监听 info 状态变量
@Monitor('info')
onChangeInfo(monitor: IMonitor) {
// 这里只监听了 'info' 一个路径,即 monitor.dirty 只有一项,可不遍历,直接通过 monitor.value()?.before 获取
monitor.dirty.forEach((path: string)=>{
let oldValue = monitor.value(path)?.before || null;
let newValue = monitor.value(path)?.now || null;
console.log('info changed, from: ' + JSON.stringify(oldValue) + ', to: ' + JSON.stringify(newValue));
})
}
// 监听 info 的 name 属性值,name 属性没有被 @Trace 装饰,监听不到
@Monitor('info.name')
onNameChanged(monitor: IMonitor) {
console.log('onNameChanged');
}
// 监听 info 的 age 属性值,age 属性被 @Trace 装饰,可被监听
@Monitor('info.age')
onAgeChanged(monitor: IMonitor) {
monitor.dirty.forEach((path: string)=>{
let oldValue = monitor.value(path)?.before || null;
let newValue = monitor.value(path)?.now || null;
console.log('info.age changed, from: ' + oldValue + ', to: ' + newValue);
})
}
build() {
Column({space: 20}) {
Text(`name: ${this.info.name}, age: ${this.info.age}`)
Button('change info')
.onClick(()=>{
// info 实例变化,即使属性值未改变,可被监听,会回调 onChangeInfo
// age 属性的值没有改变,不会触发 onAgeChanged 回调
this.info = new Info('assassin', 20);
})
Button('change name')
.onClick(()=>{
this.info.name = 'new_name'; // 监听不到
})
Button('change age')
.onClick(()=>{
// 可被监听,回调 onAgeChanged
// 在一次事件中若有多次改变,只会触发一次回调,且以最后一次修改为准: "info.age changed, from: 28, to: 128"
for (let i = 1; i <= 100; i++) {
this.info.age ++;
}
})
}.width('100%')
}
}
在 @ObservedV2 装饰的类中使用 @Monitor
使用 @Mointor 监听的属性发生变化时,会触发 @Monitor 的回调方法
- @Monitor 监听的对象属性需被 @Trace 装饰,否则属性的变化无法被监听
- @Monitor 可以监听深层属性的变化,深层属性需被 @Trace 装饰
- 在继承类场景下,可在继承链中对同一个属性进行多次监听,即若父、子类中对同一属性都设置了监听,该属性变化时,会触发所有的监听回调
- 在嵌套类场景下,若在嵌套类中对内置类的属性 (如: info.age) 进行了监听 ,同时在内置类中也对该属性设置了监听,则属性变化时,会同时触发监听回调
@ObservedV2
class Info {
@Trace name: string = 'Assassin';
@Trace public age: number = 28;
// 监听 name 属性的变化
@Monitor('age')
onAgeChanged(monitor: IMonitor) {
monitor.dirty.forEach((path: string)=>{
let oldValue = monitor.value(path)?.before || null;
let newValue = monitor.value(path)?.now || null;
console.log('age changed, from: ' + oldValue + ', to: ' + newValue);
})
}
}
// 嵌套类
@ObservedV2
class Message {
@Trace info: Info = new Info();
// 监听 info.age 属性值的变化
@Monitor('info.age')
onAgeChanged(monitor: IMonitor) {
monitor.dirty.forEach((path: string)=>{
let oldValue = monitor.value(path)?.before || null;
let newValue = monitor.value(path)?.now || null;
console.log('message.info.age changed, from: ' + oldValue + ', to: ' + newValue);
})
}
}
// 继承类
@ObservedV2
class InfoChild extends Info {
// 继承类监听 age 属性
// age 是父类 Info 的属性,可分别在父类 Info 和子类 InfoChild 定义监听,若 age 发生变化,两个类中的监听都会触发回调
@Monitor('age')
onAgeChanged(monitor: IMonitor) {
monitor.dirty.forEach((path: string)=>{
let oldValue = monitor.value(path)?.before || null;
let newValue = monitor.value(path)?.now || null;
console.log('child age changed, from: ' + oldValue + ', to: ' + newValue);
})
}
}
@Entry
@ComponentV2
export struct MainPage {
// info 没有被任何装饰器装饰,但由于 Info 类被 @ObservedV2 装饰,且 age 属性被 @Trace 装饰
// info.age 的改变会触发 UI 刷新
info: Info = new Info();
message: Message = new Message();
infoChild:InfoChild = new InfoChild();
build() {
Column({space: 20}) {
Button('change info.age: ' + this.info.age)
.onClick(()=>{
// 可触发 UI 刷新,也可被监听
this.info.age ++;
})
Button('change message.info.age: ' + this.message.info.age)
.onClick(()=>{
// message 类中监听了 info.age 的变化,会回调 Message 中的 onAgeChanged
// Info 类中也监听了 age 属性,也会触发 Info 中的监听回调
this.message.info.age ++;
})
Button('change childInfo.age: ' + this.infoChild.age)
.onClick(()=>{
// infoChild 是继承类,InfoChild 和 Info 都设置了对 age 属性的监听
// infoChild.age 的修改,会先后触发父类和子类中定义的 Monitor 回调
this.infoChild.age ++;
})
}.width('100%')
}
}
@Monitor 监听数组
- @Monitor 监听整体数组时,只能观测到数组整体的赋值,可通过监听数组的长度来判断是否有插入、删除等变化,无法监听数组中某一项的更改
- 若要监听内置类的变化,需指定监听某项的内置类的属性,如:
infoArr.0.name
@ObservedV2
class Info {
@Trace name: string = 'Assassin';
@Trace public age: number = 28;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
@Entry
@ComponentV2
export struct MainPage {
// 在 @ComponentV2 中,只有被 V2 装饰器装饰的变量可被监听
@Local infoArr: Info[] = [new Info('Tony', 34), new Info('CR7', 38)]
// 监听整个数组时,单个属性的变化无法,不会触发回调
@Monitor('infoArr')
onInfoArrChanged(monitor: IMonitor) {
monitor.dirty.forEach((path: string) => {
// 监听了两个属性值,
console.log('onInfoArrChanged',
`infoArr path: ${path} chang from ${monitor.value(path)?.before} to ${monitor.value(path)?.now}`);
})
}
// 监听 infoArr 的长度变化
@Monitor('infoArr.length')
onInfoArrLengthChanged(monitor: IMonitor) {
monitor.dirty.forEach((path: string) => {
// 监听了两个属性值,
console.log('onInfoArrLengthChanged',
`infoArr path: ${path} chang from ${monitor.value(path)?.before} to ${monitor.value(path)?.now}`);
})
}
// Info 类中属性 name、age 均被 @Trace 装饰,能够监听到变化
// 监听 infoArr 中指定项的属性 infoArr[0].name 和 infoArr[1].age
@Monitor('infoArr.0.name', 'infoArr.1.age')
onInfoArrPropertyChanged(monitor: IMonitor) {
monitor.dirty.forEach((path: string) => {
// 监听了两个属性值,
console.log('onInfoArrPropertyChanged',
`infoArr path: ${path} chang from ${monitor.value(path)?.before} to ${monitor.value(path)?.now}`);
})
}
build() {
Column({space: 20}) {
Button('change infoArr')
.onClick(()=>{
// 数组整体赋值,且长度发生变化,可触发 onInfoArrChanged、onInfoArrLengthChanged 回调
// 数组长度发生变化,会触发 onInfoArrLengthChanged 回调
// infoArr.0.name 属性发生变化,会触发 onInfoArrPropertyChanged 回调
// infoArr.1 不存在,不会触发 onInfoArrPropertyChanged,即 infoArr.1 虽然已不存在了,但是在 @Monitor 中记录不变
this.infoArr = [new Info('assassin', 20)];
})
Button('push infoArr')
.onClick(()=>{
// 点击 'change infoArr' 后再点击此按钮
// 数组长度发生变化,会触发 onInfoArrLengthChanged 回调
// 点击 'change infoArr' 后,infoArr.1 不存在了,不会触发 @Monitor 回调,即 infoArr.1.age 仍被记录为 38
// 这里点击改变了 infoArr.1.age,日志打印:infoArr path: infoArr.1.age chang from 38 to 50
this.infoArr.push(new Info('David', 50));
})
Button('change info property')
.onClick(()=>{
// 可触发 onInfoArrPropertyChanged 回调
this.infoArr[0].age++;
})
}.width('100%')
}
}
上述,点击 ‘change infoArr’ 按钮后,变量 infoArr 中只有一个元素了,infoArr.1 不存在,但不会触发 infoArr.1.age 的监听,即在 @Monitor 的记录中 infoArr.1.age 的值仍为 38。
接着点击 ‘push infoArr’ 按钮,添加了一个新的元素,infoArr.1.age 为 50,触发 onInfoArrPropertyChanged 监听,并打印日志:infoArr path: infoArr.1.age chang from 38 to 50
监听的生效及失效时间
| @Monitor 使用场景 | 生效时间 | 失效实现 |
|---|---|---|
| @ComponentV2 中 | 监听的状态变量创建,且 @Monitor 初始化完成后 | 组件销毁时 |
| @ObservedV2 类中 | 类的实例创建完成后 (构造函数内,实例未完成初始化,@Monitor 还未生效) | 类的实例销毁时 |
@ObservedV2 类中监听的生效及失效时间
当 @Monitor 定义在 @ObservedV2 装饰的类中时,@Monitor 会在类的实例创建完成后生效,在类的实例销毁时失效。
- 类中定义的 @Monitor 生效时间,晚于类的构造函数,早于组件的
aboutToAppear - 类中定义的 @Monitor 随着类的销毁失效,而类的实例销毁释放依赖于垃圾回收机制,因此可能出现组件已经被销毁,但是类还未及时销毁,导致类中定义的 @Monitor 仍在监听变化的情况
借助垃圾回收机制去取消 @Monitor 的监听是不稳定的,可采用以下两种方式管理 @Monitor 失效时间
- 将 @Monitor 定义在自定义组件中
- 主动置空监听的对象,使得 @Monitor 无法再监听其变化
@ComponentV2 中监听的生效及失效时间
当 @Monitor 定义在 @ComponentV2 装饰的自定义组件中时,@Monitor 会在状态变量初始化完成之后生效,并在组件销毁时失效。
@ObservedV2
class Info {
@Trace public message: string = 'not initialized';
constructor() {
console.log('Info constructor message initialized')
// 此时 @Monitor 还未生效,因此不会监听到 message 的变化
this.message = 'initialized';
}
}
@ComponentV2
struct Child {
@Param info: Info = new Info();
// 监听 Info 的 message 属性
@Monitor('info.message')
onMessageChanged (monitor: IMonitor) {
console.log(`Child message change from: ${monitor.value()?.before} to ${monitor.value()?.now}`);
}
aboutToAppear(): void {
console.log(('Child aboutToAppear'));
this.info.message = 'Child aboutToAppear';
}
aboutToDisappear(): void {
console.log(('Child aboutToDisappear'));
this.info.message = 'Child aboutToDisappear'
}
build() {
Column({space: 20}) {
Text('Child')
Button('change message in Child')
.onClick(()=>{
// @Param 装饰的对象,可在本地修改属性值,且会同步回父组件
this.info.message = 'Child click to change Message'
})
}.width('100%')
.backgroundColor(Color.Pink)
.padding(20)
}
}
@Entry
@ComponentV2
struct Index {
@Local info: Info = new Info();
@Local showChild: boolean = false;
// 监听 Info 的 message 属性
@Monitor('info.message')
onMessageChanged (monitor: IMonitor) {
console.log(`Index message change from: ${monitor.value()?.before} to ${monitor.value()?.now}`);
}
build() {
Column({space: 20}) {
Button('show/hide Child').onClick(()=>{
this.showChild = !this.showChild;
})
Button('change message in Index')
.onClick(()=>{
// 触发 Index 组件内的 onMessageChanged 监听回调
this.info.message = 'Index click to change Message';
})
if (this.showChild) {
Child({info: this.info})
}
}.width('100%')
}
}
上述例子中
- 当 Index 组件创建 Info 类实例时,触发 Info 的构造函数,日志会打印 ‘Info constructor message initialized’,但此时组件内的 @Monitor 还未初始化成功,因此不会监听到 message 的变化
- 点击显示 Child 组件时,触发组件的
aboutToAppear生命周期,在该生命周期方法内修改了 Child 的info.message的值,触发了 Child 组件的onMessageChanged监听回调;又由于变量 info 被 @Param 装饰,其可在本地修改属性值,且修改的值会同步回父组件 Index,可触发 Index 组件的onMessageChanged监听回调 - 点击关闭 Child 组件时,触发组件的
aboutToDisappear生命周期,在该生命周期方法内修改了 Child 的info.message的值,即可触发两个组件的onMessageChanged监听回调 - Child 组件销毁后,其注册的 @Monitor 监听也被解注册,message 的值的修改,仅有 Index 组件的 @Monitro 能监听变化
@SyncMonitor:状态变量修改同步监听
API23 开始,提供了 @SyncMonitor,用于对 V2 状态变量的同步监听。
@SyncMonitor 使用规则
@SyncMonitor 装饰器用于同步监听状态变量的修改,使得状态变量具有深度监听的能力
- 当观测的属性变化时,@SyncMonitor 装饰器定义的回调方法将被调用,判断属性是否变化使用的是严格相等 (===);同一事件内,当被观察的属性多次改变时,每次属性改变都会触发回调
- 单个 @SyncMonitor 装饰器能同时监听多个属性的变化 (“,” 隔开),这些属性在一次事件中同时发生变化时,只会触发一次回调
- @SyncMonitor 具有深度监听的能力,能监听 @ObservedV2 装饰的嵌套类、对象数组中指定项,以及多维数组的变化
- @SyncMonitor 支持在类中与 @ObservedV2、@Trace 配合使用
- @SyncMonitor 支持在 @ComponentV2 装饰的自定义组件中使用,未被状态变量装饰器 @Local、@Param、@Provider、@Consumer、@Computed 装饰的变量无法被 @SyncMonitor 监听到变化
- 在继承类场景中,可在父子类中对同一个属性分别定义 @SyncMonitor 进行监听,当属性变化时,父子类中定义的 @SyncMonitor 回调均会被调用
- 当 @SyncMonitor 监听整个数组时,无法监听内置类的变化,只能监听数组的整体赋值,和长度变化 (插入、删除)
@Monitor、@SyncMonitor 和 @Watch 的对比
| 用法 | @Watch | @Monitor | @SyncMonitor |
|---|---|---|---|
| 参数 | 回调方法名 | 监听状态变量名、属性名 | 监听状态变量名、属性名 |
| 装饰对象 | 监听的状态变量 | 回调方法,监听的变量变化时触发的回调方法 | 回调方法 |
| 监听类型 | 模糊监听 | 支持模糊监听和精准监听 | 支持模糊监听和精准监听 |
| 获取变更前的值 | 否 | 是 | 是 |
| 观察条件 | 被观察对象是状态变量 | 被观察对象是状态变量或 @Trace 装饰的类成员属性 | 被观察对象是状态变量或 @Trace 装饰的类成员属性 |
| 约束条件 | 只能在 @Component 自定义组件中使用 | 可在 @ComponentV2 自定义组件和 @ObservedV2 类中使用 | 可在 @ComponentV2 自定义组件和 @ObservedV2 类中使用 |
| 监听目标数 | 只能监听单个状态变量 | 能同时监听多个状态变量或属性 | 能同时监听多个状态变量或属性 |
| 通配符支持 | 否 | 默认不支持,API26 开始看通过配置项设置为支持 | API 23 开始支持 |
| 回调机制 | 立即 (同步) | 状态变更函数结束后 (异步),多次变更,只触发一次 | 立即 (同步) |
@Computed: 计算属性
当使用相同逻辑重复绑定在 UI 上时,为避免重复计算,可使用 @Computed 计算属性。但对于简单计算,不建议使用计算属性,因为计算属性本身也有开销。
@Computed 为方法装饰器,装饰 getter 方法。@Computed 会检测被计算的属性变化,当被计算的属性变化时,@Computed 只会求解一次。不建议在 @Computed 中修改变量。
@Computed 语法
@Computed
get varName(): T {
return value;
}
| @Computed方法装饰器 | 说明 |
|---|---|
| 支持类型 | getter 访问器 |
| 从父组件初始化 | 禁止 |
| 可初始化子组件 | @Param |
| 被执行的时机 | - @ComponentV2 中的 @Computed 会在自定义组件创建的时候初始化,触发 @Computed 计算 - @ObservedV2 中的 @Computed 会在 @ObservedV2 装饰的类实例创建后,异步初始化,触发 @Computed 计算 - @Computed 装饰的方法只有在初始化,和被计算的状态变量改变时,才会触发重新计算 |
| 是否允许赋值 | @Computed 装饰的属性是只读的,不允许赋值 |
@Computed 使用限制
- @Computed 为方法装饰器,仅能装饰 getter 方法
- @Computed 为状态管理V2提供的能力,只能在 @ComponentV2 和 @ObservedV2 中使用
- 只有被 V2 装饰的状态变量 (@Local、@Param 等) 或属性 (@Trace),才可触发 Computed
- @Computed 装饰的方法只有在初始化,和被计算的状态变量改变时,才会触发重新计算,不建议在 @Computed 装饰的 getter 方法中做除获取数据外其余的逻辑操作
- 在 @Computed 装饰的 getter 方法中,不能改变参与计算的属性,以防止循环计算属性导致 appfreeze
- @Computed 不能和
双向绑定!!连用,@Computed 装饰的是 getter 访问器,不会被子组件同步,也不能被赋值。开发者自己实现的计算属性 setter 不生效,且编译时报错
@Computed 使用场景
在 @ComponentV2 中使用计算属性
@Entry
@ComponentV2
export struct MainPage {
@Local num1: number = 0;
@Local num2: number = 0;
param: number = 20; //无法触发 Computed
// 计算变量相加的结果
@Computed
get addFun() {
console.log('addFun ----');
return this.num1 + this.num2 + this.param;
}
build() {
Column({space: 20}) {
Text('num1: ' + this.num1)
Text('num2: ' + this.num2)
Text('param: ' + this.param)
// 相加的结果
// num1 和 num2 状态变量发生变化,会自动重新计算,并刷新 UI
Text('addResult: ' + this.addFun)
Button('chang num1').onClick(()=>{
// 点击事件内的变化,只会触发一次计算
for(let i=0; i<10; i++) {
this.num1+=i;
}
})
Button('chang param').onClick(()=>{
// 无法触发 Computed
this.param++;
})
}.width('100%')
}
}
对于简单的计算逻辑,或计算逻辑仅使用一次,不应使用计算属性,其本身有性能开销
在 @ObservedV2 类中使用计算属性
@ObservedV2
export class Info {
name: string = 'assassin'; // 未被 @Trace 装饰,值变化无法触发 Computed
@Trace age: number = 20;
@Computed
get toJsonString() {
console.log('toJsonString ---->')
return (`{name: ${this.name}, age: ${this.age}}`);
}
}
@Entry
@ComponentV2
export struct MainPage {
@Local info: Info = new Info();
build() {
Column({space: 20}) {
Text('info: ' + this.info.toJsonString)
Button('chang age').onClick(()=>{
// 点击事件内的变化,只会触发一次计算
for(let i=0; i<10; i++) {
this.info.age ++;
}
})
Button('chang name').onClick(()=>{
// info.name 属性没有被 @Trace 装饰,其修改无法触发 Computed
this.info.name = 'Jack';
})
}.width('100%')
}
}
@Computed 装饰的属性可被 @Monitor 监听
@Entry
@ComponentV2
export struct MainPage {
@Local num1: number = 0;
@Local num2: number = 0;
// 计算变量相加的结果
@Computed
get addFun() {
console.log('addFun ----');
return this.num1 + this.num2
}
// 监听 addFun
@Monitor('addFun')
addChanged(monitor: IMonitor) {
// addFun 计算结果监听
console.log('addChanged, from: ' + monitor.value()?.before + ', to: ' + monitor.value()?.before)
}
build() {
Column({space: 20}) {
// 相加的结果
// num1 和 num2 状态变量发生变化,会自动重新计算,并刷新 UI
Text(`${this.num1} + ${this.num2} = ${this.addFun}`)
Button('chang num1').onClick(()=>{
// 点击事件内的变化,只会触发一次计算
for(let i=0; i<10; i++) {
this.num1+=i;
}
})
}.width('100%')
}
}
@Computed 装饰的属性初始化 @Param
@Entry
@ComponentV2
export struct MainPage {
@Local num1: number = 0;
@Local num2: number = 0;
// 计算变量相加的结果
@Computed
get addFun() {
console.log('addFun ----');
return this.num1 + this.num2
}
build() {
Column({space: 20}) {
// 相加的结果
// num1 和 num2 状态变量发生变化,会自动重新计算,并刷新 UI
Text(`${this.num1} + ${this.num2}`)
Child({result: this.addFun})
Button('chang').onClick(()=>{
// 点击事件内的变化,只会触发一次计算
this.num1++;
this.num2 += 2;
})
}.width('100%')
}
}
@ComponentV2
struct Child {
// 计算结果
@Param result: number = 0;
build() {
Text('result: ' + this.result);
}
}
@Type: 标记类属性的类型
@Type 用于标记类属性,配合 PersistenceV2 使用,防止序列化时类丢失,便于类的反序列化。
| @Type 装饰器 | 说明 |
|---|---|
| 装饰器参数 | type:class 类型 |
| 可装饰的类型 | Object class 以及Array、Date、Map、Set 等内嵌类型 |
使用限制
- @Type 只能在 @ObservedV2 装饰的类中使用,不能在自定义组件中使用
- 不支持构造函数函参的类
- 不支持 collections.Set、collections.Map 等类型
- 不至此简单类型,如:string、number、boolean
- 不支持非 built-in 类型,如:PixelMap、NativePointer、ArrayList 等 Native 类型
使用场景:持久化数据
import { PersistenceV2, Type } from "@kit.ArkUI";
@ObservedV2
class TestChild {
@Trace childNum: number = 1;
}
@ObservedV2
class Test {
// 对于复杂对象需要 @Type 修饰,确保反序列化成功,去掉 @Type 反序列化失败
@Type(TestChild)
// 对于没有初值的类属性,经过 @Type 修饰后,需要手动保存,否则持久化失败
// 无法使用 @Type 修饰的类属性,必须要有初值才能持久化
@Trace testChild?: TestChild = undefined;
// 没有 @Type 修饰
@Trace childNoType?: TestChild = undefined;
}
@Entry
@ComponentV2
export struct MainPage {
@Local test: Test = PersistenceV2.connect(Test, () => new Test)!;
build() {
Column({space: 20}) {
Text('childNum:' + this.test.testChild?.childNum)
.onClick(() => {
this.test.testChild = new TestChild();
this.test.testChild.childNum++;
PersistenceV2.save(Test);
})
}.width('100%')
}
}
更多推荐


所有评论(0)