HarmonyOS-ArkUI V2装饰器: @Monitor装饰器:状态变量修改监听

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监听死循环问题,在监听函数里,开发者不要修改被监听状态变量的值!会造成死循环!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

2579所以然

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值