Monitor作用
Monitor的作用就是来监听状态变量的值变化的。被Monitor修饰的函数,会在其对应监听的变量发生值的变化时,回调此函数,从而可以让您知道是什么值发生变化了,变化前是什么值,变化后是什么值。
V1版本的装饰器,有个叫@Watch的装饰器,其实也有监听变化的能力,但是其重要局限就是,不能知道是什么参数发生了变化,也不知道参数变化之前是什么值。在代码中大致反应如下:
class Info{
name: string = 'Tom'
age: number = 25
}
@Entry
@Component
struct MonitorTest {
@State @Watch('onInfoChange') info: Info = new Info()
@State @Watch('onNumberArrayChange') numArr: number[] = [1, 2, 3, 4, 5]
//这个函数没有参数.所以值变成了什么您可以通过查看当前值来获取,但是之前是怎么样就不知道了.
//而且到底是info name改变引发的, 还是info age改变引发的也没办法知道.
onInfoChange(){
hilog.info(LOG_DOMAIN, LOG_TAG, `info after change info.name=${this.info.name}, info.age=${this.info.age}`)
}
onNumberArrayChange(){
hilog.info(LOG_DOMAIN, LOG_TAG, `numArr after change! numArr=${JSON.stringify(this.numArr)}}`)
}
build() {
Row() {
Column() {
Button("change info name")
.onClick(() => {
this.info.name = "Jack";
})
Button("change info age")
.onClick(() => {
this.info.age = 30;
})
Button("change numArr[2]")
.onClick(() => {
this.numArr[2] = 5;
})
Button("change numArr[3]")
.onClick(() => {
this.numArr[3] = 6;
})
}
.width('100%')
}
.height('100%')
}
}
watch有多不好用,那么在设计V2版本时,是一定要改掉那些缺陷的。Monitor就是对一个Watch的修正。而且它具备以下能力
- 同时监听很多属性的变化,并且当这些属性如果在同一个逻辑场景下,同时改变值的时候,那么就会回调一次相关的监听方法。类似于把变化当成事务打包处理。
- 如果被监听的对象是一个类,或者复杂的内嵌类型,且这个对象被@ObservedV2和@Trace修饰,也就是对象已经具备被深度观测的能力了,那么Monitor是可以做到深度监听对象的能力的。
Monitor简单用法
这个Monitor的使用方式有一点点复杂,我们先看一个案例,体会一下,之后再扩开了解吧。
@Entry
@ComponentV2
struct MonitorTest{
@Local name:string = '王小二'
@Local age:number = 18
@Monitor('name', 'age') //修饰函数,代表对什么变量感兴趣.此处容易出bug.就是改变量名一个不小心少改这块就会出问题.
onStrChange(monitor: IMonitor) { //此函数的协议是要带一个IMonitor参数.
hilog.info(LOG_DOMAIN, LOG_TAG, "回调onStrChange")
monitor.dirty.forEach((path: string)=>{
hilog.info(LOG_DOMAIN, LOG_TAG, "回调onStrChange, 循环当前Path=" + path)
hilog.info(LOG_DOMAIN, LOG_TAG, `${path} change from ${monitor.value(path)?.before} to ${monitor.value(path)?.now}`)
})
}
build() {
Column(){
Button('change string')
.onClick(()=>{
this.name = '王小三'
hilog.info(LOG_DOMAIN, LOG_TAG, "改完name")
this.age = 15
hilog.info(LOG_DOMAIN, LOG_TAG, "改完age") //此逻辑执行完成之后,打包完成后再进行同步数据的.具有一定的事务的特征
})
}
}
}
打印日志:
Monitor使用协议介绍
根据上方的代码,我们大概知道了Monitor应该是怎么用的,也应该能猜测的出这个装饰器是干什么的了。下面讲讲具体的使用规则。
装饰器说明
装饰器名 |
装饰器参数 |
装饰对象 |
@Monitor |
字符串数组,内容是要监听的当前组件中的状态变量名称。 如:@Monitor('name', 'age') 可监听深层的属性变化 |
装饰组件的成员方法。当值变化时就会回调。 该方法一定是:只有一个参数,且参数类型是IMonitor。如代码所示。 |
@Monitor装饰器用于监听状态变量的修改,注意,这里的前提是,监听的是状态变量。其监听的状态范围为, 被 @Local, @Param, @Provider, @Consumer, @Computed 修饰的状态变量
- @Monitor是V2装饰器里面的,所以它一定要配套的是V2的装饰器,支持在@ComponentV2装饰的自定义组件中使用。
- @Monitor还可以用在我们自定义的类中,但是这个类要与@ObservedV2和@Trace配合使用。来确定哪些变量是允许深度监测的。
- 在继承类场景中,可以在父子组件中对同一个属性分别定义@Monitor进行监听,当属性变化时,父子组件中定义的@Monitor回调均会被调用。
- 当被观测的状态变量发生变化时,@Monitor修饰的回调方法将会被回调。判断属性是否变化使用的是严格相等(===),当严格相等判断的结果是false(即不相等)的情况下,就会触发@Monitor的回调。当在一次事件中多次改变同一个属性时,将会使用初始值和最终值进行比较以判断是否变化。
- 单个@Monitor可以同时监测很多个属性的变化,只要您把它们在参数中列出来即可。当这些属性在一次事件中共同变化时,只会触发一次@Monitor的回调方法。
- 如果被监听的对象是一个类,或者复杂的内嵌类型,且这个对象被@ObservedV2和@Trace修饰,也就是对象已经具备被深度观测的能力了,那么Monitor是可以做到深度监听对象的能力的。
IMonitor类关系
Monitor修饰的函数,有一个唯一的参数,是IMonitor类型的。如下文代码。
@Monitor('name', 'age') //修饰函数,代表对什么变量感兴趣.此处容易出bug.就是改变量名一个不小心少改这块就会出问题.
onStrChange(monitor: IMonitor) { //此函数的协议是要带一个IMonitor参数.
hilog.info(LOG_DOMAIN, LOG_TAG, "回调onStrChange")
monitor.dirty.forEach((path: string)=>{
hilog.info(LOG_DOMAIN, LOG_TAG, "回调onStrChange, 循环当前Path=" + path)
hilog.info(LOG_DOMAIN, LOG_TAG, `${path} change from ${monitor.value(path)?.before} to ${monitor.value(path)?.now}`)
})
}
我们发现,拿到这个IMonitor的时候,处理还是蛮复杂的,似乎一层套一层。下面分析下这个类的关系,看看里面有啥。
当回调来的时候,传回的IMonitor实例,里面含有dirty变量,dirty变量是一个字符串数组,记录了刚才所有变更了的变量名字。当我们拿到这个变量名字之后,可以通过调用其 value函数查找到相关变量变更的详细信息。
该信息是IMonitorValue类型的,里面记录了变更前数据 befor,变更后数据 now, 和对应的变量名字。
Monitor与Trace配合实现对对象的深度观测能力
Monitor本身并不具备深度观测的能力,它能检测的也就是状态变量本身有没有被赋值这类比较大的操作。如果您想感知某个复杂对象的某个变量的变化,需要结合@ObservedV2和@Trace装饰器,先使得被观察对象具备深度检测能力。再在修饰的函数中指定要观测的属性名称。
@ObservedV2 //使对象可被深度观测
class Info{
@Trace name: string
@Trace age: number
constructor(name: string, age: number) {
this.name = name
this.age = age
}
}
@Entry
@ComponentV2
struct MonitorTest{
@Local info: Info = new Info("王小四", 14)
//如果要深度检测变量,那么要写好变量的整条链路才可以,所以我们加上了info.name
@Monitor('info', 'info.name')
onInfoChange(monitor:IMonitor) {
monitor.dirty?.forEach((name: string) => {
hilog.info(LOG_DOMAIN, LOG_TAG,`onInfoChange, name=${name}, befor=${monitor.value(name)?.before}, now=${monitor.value(name)?.now}`)
})
}
build() {
Column() {
Text(`name: ${this.info.name}, age: ${this.info.age}`)
Button("change info")
.onClick(() => {
this.info = new Info("Lucy", 18); // 能够监听到
})
Button("change info.name")
.onClick(() => {
this.info.name = "Jack"; // 监听不到
})
}
}
}
在@ObservedV2装饰的类中使用@Monitor装饰器
@Monitor装饰器也可以用来专门监听类中的数据。不过这个类要用@Monitor装饰器修饰,配合@Trace来使用。类中写监听,会增强复用能力。
@ObservedV2
class Info{
@Trace name: string = "Tom"
@Trace region: string = "North";
@Trace job: string = "Teacher";
age: number = 25
// 类中写了监视器,那么就可以变为共用的了,可以复用.
@Monitor("name")
onNameChange(monitor: IMonitor) {
hilog.info(LOG_DOMAIN, LOG_TAG, `name change from ${monitor.value()?.before} to ${monitor.value()?.now}`)
}
@Monitor("region", "job")
onRegionJobChange(monitor: IMonitor) {
monitor.dirty.forEach((path: string) => {
console.log(`onRegionJobChange ${path} change from ${monitor.value(path)?.before} to ${monitor.value(path)?.now}`);
})
}
}
@Entry
@ComponentV2
struct MonitorTest {
info: Info = new Info();
build() {
Column() {
Button("change name")
.onClick(() => {
this.info.name = "Jack"; // 能够触发onNameChange方法
})
Button("change age")
.onClick(() => {
this.info.age = 26; // 不能够触发onAgeChange方法
})
Button("change region")
.onClick(() => {
this.info.region = "South"; // 能够触发onChange方法
})
Button("change job")
.onClick(() => {
this.info.job = "Driver"; // 能够触发onChange方法
})
}
}
}
Monitor在类中使用,实现深度监测能力: 与Trace配合使用
@Monitor在类中照样能和像组件里那样实现对象的深度监测能力。有两个条件要达成
- 被深度监测的变量一定要是被Trace修饰的变量
- Monitor装饰器的参数要指对。深度监测的变量是要向调代码那样表明是什么句柄的什么参数的,如以下代码所示。
@ObservedV2
class Inner{
@Trace num: number = 0 //要被trace修饰,以使对象可以接收被深度监测
}
@ObservedV2 //这里面明明没有trace修饰的变量却加这个装饰器,原因是@Monitor需要在@ObservedV2里,不然报错
class Outer{
inner: Inner = new Inner()
@Monitor('inner.num')
onChange(monitor: IMonitor) {
console.log(`inner.num change from ${monitor.value()?.before} to ${monitor.value()?.now}`);
}
}
@Entry
@ComponentV2
struct MonitorTest {
outer: Outer = new Outer();
build() {
Column() {
Button("change name")
.onClick(() => {
this.outer.inner.num = 100; // 能够触发onChange方法
})
}
}
}
在继承类中,可以在不同层次的子类父类对同一个属性完成监听
@ObservedV2
class Base {
@Trace name: string;
// 基类监听name属性
@Monitor("name")
onBaseNameChange(monitor: IMonitor) {
console.log(`Base Class name change`);
}
constructor(name: string) {
this.name = name;
}
}
@ObservedV2
class Derived extends Base {
// 继承类监听name属性
@Monitor("name")
onDerivedNameChange(monitor: IMonitor) {
console.log(`Derived Class name change`);
}
constructor(name: string) {
super(name);
}
}
@Entry
@ComponentV2
struct Index {
derived: Derived = new Derived("AAA");
build() {
Column() {
Button("change name")
.onClick(() => {
this.derived.name = "BBB"; // 能够先后触发onBaseNameChange、onDerivedNameChange方法
})
}
}
}
@Monitor监听生效失效时间
自定义组件中@Monitor时效范围
也就是监听具有时效性,创建是我们自己定义的,销毁是系统在组件销毁的时候替我们销毁的。这样能替我们避免很多关于内存泄漏的问题。
具体是: @Monitor定义在@ComponentV2装饰的自定义组件中时,@Monitor会在状态变量初始化完成之后生效,并在组件销毁之时失效。
@ObservedV2
class Info{
@Trace message: string = "默认值"
constructor() {
hilog.info(LOG_DOMAIN, LOG_TAG, "Info 初始化函数调用")
this.message = "初始化函数中被初始化的值"
}
}
@ComponentV2
struct Child5{
@Param info: Info = new Info()
@Monitor("info.message")
onMessageChange(monitor: IMonitor) {
console.log(`Child 收到info.message改动 从 ${monitor.value()?.before} 变成 ${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() {
Text("Child")
Button("点击改值")
.onClick(() => {
this.info.message = "Child组件中点击按钮改的值";
})
}
.borderColor(Color.Red)
.borderWidth(2)
}
}
@Entry
@ComponentV2
struct MonitorTest{
@Local info: Info = new Info()
@Local flag: boolean = false
@Monitor("info.message")
onMessageChange(monitor: IMonitor) {
console.log(`MonitorTest 界面接收变化 message change from ${monitor.value()?.before} to ${monitor.value()?.now}`);
}
build() {
Column(){
Button("点击显示/隐藏子组件")
.onClick(()=>{
this.flag = !this.flag
})
Button("改Info里的值")
.onClick(() => {
this.info.message = "父组件点击改的值";
})
if (this.flag) { //条件渲染语句,不断触发组件的创建销毁,用来观测monitor检测生效时段
Child5({ info: this.info })
}
}
}
}
类中@Monitor对变量监听的时效范围
在类创建完成后生效,类销毁时失效。案例省略了,太简单。不过我们在这里需要格外注意一个问题,就是类中写的监听,有时候会引发组件销毁类依然监听的问题。因为类的实际销毁是依赖于垃圾回收机制的,跟组件销毁的时机是不一样的。所以会出现这种变化。
那么怎么避免这种问题呢?
我们可以在组件销毁的时候,将含有@Monitor装饰的变量置为 undefined!
如下列代码所示:
@ObservedV2
class InfoWrapper {
info?: Info;
constructor(info: Info) {
this.info = info;
}
@Monitor("info.age")
onInfoAgeChange(monitor: IMonitor) {
console.log(`age change from ${monitor.value()?.before} to ${monitor.value()?.now}`)
}
}
@ObservedV2
class Info {
@Trace age: number;
constructor(age: number) {
this.age = age;
}
}
@ComponentV2
struct Child {
@Param @Require infoWrapper: InfoWrapper;
aboutToDisappear(): void {
console.log("Child aboutToDisappear", this.infoWrapper.info?.age)
this.infoWrapper.info = undefined; // 使InfoWrapper对info.age的监听失效
}
build() {
Column() {
Text(`${this.infoWrapper.info?.age}`)
}
}
}
@Entry
@ComponentV2
struct Index {
dataArray: Info[] = [];
@Local showFlag: boolean = true;
aboutToAppear(): void {
for (let i = 0; i < 5; i++) {
this.dataArray.push(new Info(i));
}
}
build() {
Column() {
Button("change showFlag")
.onClick(() => {
this.showFlag = !this.showFlag;
})
Button("change number")
.onClick(() => {
console.log("click to change age")
this.dataArray.forEach((info: Info) => {
info.age += 100;
})
})
if (this.showFlag) {
Column() {
Text("Childs")
ForEach(this.dataArray, (info: Info) => {
Child({ infoWrapper: new InfoWrapper(info) })
})
}
.borderColor(Color.Red)
.borderWidth(2)
}
}
}
}
应用案例
自定义style
写一个只要改了某个状态相关的值,整体界面就会变成相应的样子。
@ObservedV2
class Info{
@Trace value: number = 50
}
@ObservedV2
class MyStyle{
info: Info = new Info()
@Trace color:Color = Color.Black
@Trace fontSize: number = 45
@Monitor("info.value")
onValueChange(monitor: IMonitor) {
let old: number = monitor.value()?.before as number
let now: number = monitor.value()?.now as number
if (old != 0) {
let diffPercent: number = (now - old) / old;
if (diffPercent > 0.1) {
this.color = Color.Red;
this.fontSize = 50;
} else if (diffPercent < -0.1) {
this.color = Color.Green;
this.fontSize = 40;
} else {
this.color = Color.Black;
this.fontSize = 45;
}
}
}
}
@Entry
@ComponentV2
struct MonitorTest {
textStyle: MyStyle = new MyStyle();
build() {
Column() {
Text(`Important Value: ${this.textStyle.info.value}`)
.fontColor(this.textStyle.color)
.fontSize(this.textStyle.fontSize)
Button("change!")
.onClick(() => {
this.textStyle.info.value = Math.floor(Math.random() * 100) + 1;
})
}
}
}
限制
- 不建议在一个类中对同一个属性进行多次@Monitor监听。如果您这样做,那么只有最后一个监听方法生效
- @Monitor的参数,不能为变量。尽可以使用字符串字面量,const常量,enum枚举值。如果您非要传入变量,那么Monitor只根据最初的那个值来,以后怎么改动也不会奏效的。因此不建议开发者使用变量作为@Monitor的参数
- @Monitor监听死循环问题,在监听函数里,开发者不要修改被监听状态变量的值!会造成死循环!