@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);
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐