在这里插入图片描述

@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装饰器:状态变量修改异步监听

@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:状态变量修改同步监听

@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: 计算属性

@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%')
	  }
	}

Logo

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

更多推荐