ArkUI -- @ObservedV2 和 @Trace: 类属性变化观测 (状态管理 V2)
本文介绍了HarmonyOS中@ObservedV2和@Trace装饰器的使用方法及其特性。这两个装饰器配合使用可以实现对嵌套类对象属性变化的直接观察,解决了V1版本状态管理在复杂场景下的局限性。文章详细说明了装饰器的使用限制、嵌套类和继承类场景的应用示例,以及对象序列化/反序列化的注意事项。特别强调了通过class-transformer库实现反序列化后仍保持观测能力的方法,并提供了完整的代码示
@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 的内置类型时,可观测到集合内置类的变化
使用限制
- @Trace 是 class 中属性的装饰器,不能用在 struct 中
- @ObservedV2 仅能装饰 class,无法装饰自定义组件
- 使用 @ObservedV2 与 @Trace 装饰的类不能和 @State 等V1的装饰器混合使用,编译时报错,需用 @Local 等V2的装饰器
- 继承自 @ObservedV2 的类无法和 @State 等V1的装饰器混合使用,运行时报错
- @ObservedV2 的类实例无法直接使用 JSON.parse 反序列化获得 (直接使用 JSON.parse 反序列化获得的对象无法观察属性变化)
使用场景
嵌套类场景
// 嵌套类必须被 @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 = '';
}
class Login{
user: User = new User();
}
@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 装饰对象数组
@ObservedV2
class User {
// 被 @Trace 装饰,值的变化可触发 UI 刷新
@Trace age: number;
constructor(age: number) {
this.age = age;
}
}
@ObservedV2
class Info {
// @Trace 装饰对象数组,对象的类也需要被 @ObservedV2 和 @Trace 装饰
@Trace userList: Array<User> = [];
id: number = 0;
constructor() {
this.id = 1;
this.userList = [new User(10), new User(20), new User(30)];
}
}
@Entry
@ComponentV2
export struct MainPage {
@Local info: Info = new Info();
build() {
Column({space: 20}) {
Button('change age: ' + this.info.userList[0].age).onClick(()=> {
this.info.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));
UIUtils 辅助接口
导入 UIUtils:
import { UIUtils } from '@kit.ArkUI';
makeObserved: 将非观察数据变为可观察数据
@ObservedV2 和 @Trace 的观测能力,依赖 new 操作创建的对象实例,直接使用
JSON.parse()生成的是普通对象,没有经过代理包装,属性的变化无法被框架感知,也就不会触发 UI 更新
对没有__ob_前缀的 JSON 字符串,使用JSON.parse()生成的是普通对象,无法观察,不会触发 UI 更新
上述,三方库 class-transformer 用于将 @ObservedV2 序列化后的再反序列化,可得到原始对象 (对象的属性名不变),且可观察。
makeObserved,用于将非观察对象转为可深度观察,主要用于 @ObservedV2/@Trace 无法涵盖的场景:
- class 定义在三方包中,无法手动对 class 中需要观察的属性加上 @ObservedV2/@Trace 装饰器
- @Trace 不可用于 @Sendable 装饰的类
- interface 或 JSON.parse 返回的匿名对象无法被观测,可使用
makeObserved将其转化为可观察对象 - 在使用 PersistenceV2 持久化
Array<ClassA>类型的数据时,需用makeObserved使返回的对象被观察到
makeObserved 接口实现的是深度观察,其会通过代理模式监听整个对象数,因此对嵌套对象的属性的修改,可触发 UI 更新
makeObserved 的使用
UIUtils 提供了 makeObserved 静态接口,可将普通不可观察数据变为可观察数据。
// 传入数据源,返回可观察对象
static makeObserved<T extends object>(source: T): T
source 为数据源对象,支持如下类型
- 支持未被 @Observed 或 @ObservedV2 装饰的类
- 支持 JSON.parse 返回的 Object
- 支持 @Sendable 装饰的类
- 支持 Array、Map、Set 和 Date
- 支持 collections.Array,collections.Set 和 collections.Map
makeObserved 不支持的传参类型说明如下:
| 不支持的类型 | 说明 |
|---|---|
| undefined、null | 直接返回,不做任何处理 |
| 非 Object 类型 | 编译拦截报错 |
| @ObservedV2/@Observed 装饰的类 | 为避免数据被双重代理,直接返回,不做任何处理 |
| makeObserved 封装过的代理对象 | 为避免数据被双重代理,直接返回,不做任何处理 |
| 状态管理 V1 的状态变量 | 直接返回,不做任何处理,即观察规则不变,如: 传入 @State 的状态变量,返回的对象仍只能观察到第一层的变化 |
makeObserved 不能和 V1 的状态管理器一起使用,但可在 @Component 装饰的自定义组件里使用
【示例】
import { UIUtils } from "@kit.ArkUI";
class Info {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
@Entry
@ComponentV2
export struct MainPage {
// 普通对象实例
info: Info = new Info('info', 30);
// 将 new 创建的实例转为可观察对象
// observedInfo 被 @Local 装饰,本身具有观察自身赋值的能力,而其值为 makeObserved 的返回值,具有深度观察能力
// makeObserved 仅对 observedInfo 进行深度观察,而 observedInfo 自身赋值的变化,则是有 @Local 观察的
@Local observedInfo: Info = UIUtils.makeObserved(new Info('Assassin', 20));
build() {
Column({space: 20}) {
Button('change age: ' + this.info.age)
.onClick(()=>{
// info 是普通对象实例,不可观察,也不会触发 UI 刷新
this.info.age ++
})
Button('change age: ' + this.observedInfo.age)
.onClick(()=>{
// observedInfo 是可观察对象实例,可触发 UI 刷新
this.observedInfo.age ++;
})
Button('new observedInfo').onClick(()=>{
this.observedInfo = new Info('NEW', 0);
})
Button('makeObserved').onClick(()=>{
this.observedInfo = UIUtils.makeObserved(this.observedInfo)
})
}.width('100%')
}
}
makeObserved 仅对入参对象进行深度观察
上述 observedInfo 变量被 @Local 装饰,具有观察自身赋值的能力,而其值为 makeObserved 的返回值,具有深度观察能力。
当点击 ‘new observedInfo’ 按钮后,重新创建了一个新的 Info 实例,则失去了深度观察能力,但是由于 observedInfo 被 @Local 装饰,其仍可感知自身赋值的变化,会刷新 UI,即点击按钮后 this.observedInfo.age 显示为 0;
但由于其失去了深度观察能力,再次点击 ‘change age’ 按钮,不会刷新 UI,需点击 ‘makeObserved’ 按钮,令新创建的实例获取深度观察能力后,点击 ‘change age’ 才会刷新 UI。
makeObserved 和 @Sendable 配合使用
@Sendable 主要用于处理并发任务。
makeObserved 和 @Sendable 配合使用,可以实现在子线程处理数据,在 UI 线程处理 ViewModel 的显示和观察数据的需求。
需要注意的是,数据的构建和处理可以在子线程中完成,但有观察能力的数据 (makeObserved 返回的) 只能在主线程里操作,不能传给子线程,只能将需要的属性传给子线程
【示例】
// entry\src\main\ets\model\SendableData.ets
// @Sendable 支持异步线程的对象
@Sendable
export class SendableData {
name: string = 'Assassin';
age: number = 20;
}
// 提供一个异步方法,延迟 3s 后,返回一个 name 为 'fetchData',age+1 的 SendableData 实例
export async function fetchData(age: number): Promise<SendableData> {
return new Promise((resolve)=>{
setTimeout(()=>{
let data: SendableData = new SendableData();
data.name = 'fetchData';
data.age = ++age;
resolve(data);
}, 3000)
})
}
import { UIUtils } from "@kit.ArkUI";
import { taskpool } from "@kit.ArkTS";
import { fetchData, SendableData } from "../model/SendableData";
// 子线程执行
// 根据传入的 age 创建并返回一个 SendableData 实例
@Concurrent
async function threadExecute(age: number): Promise<SendableData> {
let data: SendableData = await fetchData(age);
return data;
}
@Entry
@ComponentV2
export struct DemoPage {
// @Sendable 在 Preview 下会报类型错误,只能在真机上运行
// 通过 makeObserved 给 @SendableData 对象添加可观察能力
@Local send: SendableData = UIUtils.makeObserved(new SendableData());
build() {
Column({space: 20}) {
Text(this.send.name + '_' + this.send.age)
Button('change age').onClick(()=>{
// 修改属性值,可以被观察到,会刷新 UI
this.send.age ++;
})
// 子线程执行
// 由于观察的数据只能在主线程内操作,不能传给子线程,这里只传递 age 属性给子线程
// 子线程执行 threadExecute,会返回一个新创建的实,如需其仍可深度观察,需再次 makeObserved
Button('task').onClick(()=>{
taskpool.execute(threadExecute, this.send.age).then(val =>{
this.send = UIUtils.makeObserved(val as SendableData);
})
})
}.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
makeObserved 和 V2装饰器配合使用
makeObserved 可以和 V2 装饰器一起使用,但是由于传入 @ObservedV2 装饰的类,会返回其自身。所以,V2 装饰器 @Monitor 或 @Computed 不能定义在 class 中,只能定义在自定义组件内。
而对于没有被 @Observed 装饰的普通对象,即使添加了 @Monitor 监听,和 @Computed 回调,其属性改变,也不会触发这些回调。
通过 makeObserved 将其转化为可观察对象后,属性的改变才会触发 @Monitor 监听 和 @Computed 回调。
import { UIUtils } from "@kit.ArkUI";
class Info {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
@Entry
@ComponentV2
export struct DemoPage {
// info 只是一个普通类,在 点击 'change info' 按钮前,age 的改变,不会触发 @Monitor 和 @Computed 的回调
// 点击 'change info' 按钮后天,通过 makeObserved 返回一个可观察对象后,再次改变 age 属性,回调会被触发
@Local info: Info = new Info('Assassin', 20);
// 监听 info.age
@Monitor('info.age')
onAgeChanged(monitor: IMonitor) {
console.log(`info.age change, from: ${monitor.value()?.before}, to: ${monitor.value()?.now}`)
}
// 当 age 或 name 发生变化,需重新计算时,触发该回调
@Computed
get infoStr() {
console.log('computed -----');
return this.info.name + '_' + this.info.age;
}
build() {
Column({space: 20}) {
Text('computed: ' + this.infoStr)
Button('change age: ' + this.info.age).onClick(()=>{
this.info.age ++;
})
Button('change info').onClick(()=>{
this.info = UIUtils.makeObserved(new Info('test', 10));
})
}.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
getTarget: 获取状态管理代理的原始对象
状态管理框架会对 class、Date、Map、Set、Array 类型的原始对添加代理,用于观测属性变化与 API 调用。这一层代理会使得变量类型改变,因而在类型判断、NAPI 调用等场景,会由于类型的改变产生预料之外的结果。
UIUtils 提供了 getTarget 接口,其可获取状态管理框架代理前的原始对象,即 makeObserved 封装的代理对象,或状态管理 V1 和 V2 装饰的对象 (状态管理框架会为装饰的对象添加一层代理,用于观察变化),可通过 getTarget 获取到其原始对象,对原始对象的赋值不会触发 UI 刷新。
// 返回数据源对象去除状态管理框架所加代理后的原始对象
static getTarget<T extends object>(source: T): T
- getTarget 仅支持对象类型传参,非对象类型入参,编译时报错
- 更改 getTarget 获得的原始对象中的内容,不会被观察到,也不会触发 UI 刷新
【示例】
// 可被观察对象
@Local observedInfo: Info = UIUtils.makeObserved(new Info('Assassin', 20));
// 取消代理的原始对象,不可被观察,不会刷新 UI
@Local originInfo: Info = UIUtils.getTarget(this.observedInfo);
更多推荐


所有评论(0)