鸿蒙-状态管理V2

前言

前面我们讲了状态管理V1相关装饰器和注意事项,一般来讲是足够在开发中使用了,但还是有一些不方便的地方,因此又有了状态管理 V2。 截止到现在,V2的指南中已经移除了 gap提示,说明已经稳定可用。官方也希望我们尽快做迁移,当然,将来会不会出 V3 那就不得而知了。

综述

V2 版本的状态管理装饰器有以下几种,都是从api12开始支持。不过问题不大,目前应用市场上架鸿蒙应用最低版本要求就是api12

  • @ComponentV2装饰器:自定义组件
  • @ObservedV2装饰器和@Trace装饰器
  • @Local装饰器:组件内部状态
  • @Param:组件外部输入
  • @Once:初始化同步一次
  • @Event装饰器:规范组件输出
  • @Provider装饰器和@Consumer装饰器:跨组件层级双向同步
  • @Monitor装饰器:状态变量修改监听
  • @Computed装饰器:计算属性

@ComponentV2

和V1中的@Component装饰器一样,@ComponentV2装饰器用于装饰自定义组件:只有在该装饰器修饰的类中才能使用,也仅能使用其他V2版本的状态管理装饰器,无法在同一个自定义组件中混用V1、V2装饰器

TypeScript 复制代码
@ComponentV2 // 装饰器
struct Index { // struct声明的数据结构
  build() { // build定义的UI
  }
}

@ObservedV2和@Trace

先来看下这两个,后续介绍其他装饰器时有用到。 这两个装饰器被用来做深度观测,也就是嵌套类的变化。在 V1 版本中,我们使用@Observed@ObjectLink来实现,但有一个问题就是无法进行嵌套观测,只能观测当前层级的属性变化。V2 版本的这两个装饰器配合使用,可以实现跨嵌套层级观测。

TypeScript 复制代码
@ObservedV2
class Person {
  @Trace name: string = ''
  @Trace age: number = 0
  @Trace address:Address = new Address()
}

@ObservedV2
class Address{
  @Trace zipCode : string = '000000'
  @Trace city:string = '北京'
}

定义一个嵌套类,使用@ObservedV2装饰需要观测的类,并且在类中使用@Trace修饰每一个需要参与UI绘制的属性。当需要观测的类和属性较多时,写起来就比较恶心了。 还有一个非常恶心也非常麻烦的点:@ObservedV2的类实例目前不支持使用JSON.stringify进行序列化。

@Local

该装饰器修饰的变量只能 在本地初始化,不能由外部传入,目的是能更好的表示组件内部的状态,不会被外部传入参数影响。 和@State相比,它无法观察到class对象属性的赋值,仅能观察到对象的整体赋值,也就是说它的观测能力仅限于被装饰变量的本身。 当需要观察class对象属性的变化时,需要使用@ObservedV2@Trace装饰器。 有一点需要注意的地方:在状态管理V2中,会给使用状态变量装饰器如@Trace、@Local装饰的Date、Map、Set、Array添加一层代理用于观测API调用产生的变化,因此我们在取其中的值进行比较时,需要使用UIUtils.getTarget()获取原始对象进行比较。

TypeScript 复制代码
list: string[][] = [['a'], ['b'], ['c']];
@Local strList: string[] = this.list[0];
@Monitor("strList")
onStrChange(monitor: IMonitor) {
    hilog.error(0x01, '@Local', 'strList has changed')
}

build() {
    Column() {
          Button('修改为同一个对象').onClick(() => {

        if (this.strList !== this.list[0]) {
          hilog.error(0x01, '@Local', '重新赋值')
          this.strList = this.list[0];
        }
      })
      Button('修改为同一个对象').onClick(() => {

        if (UIUtils.getTarget(this.strList) !== this.list[0]) {
          hilog.error(0x01, '@Local', '重新赋值')
          this.strList = this.list[0];
        }
      })
    }
}

这里我们定义了一个二维数组,同时将数组第一项赋值给被@Local修饰的变量,同时使用@Monitor观察该变量的变化。 当我们点击第一个按钮时,发现控制台会打印重新赋值strList has changed。当我们点击第二个按钮时,控制台没有打印。

@Param

可以从父组件传入,也可以在本地初始化,如果配合@Require使用,则父组件必须传入该参数,此时本地初始化的值将被覆盖。 有一点需要注意:不能在组件内部直接修改变量本身,但可以修改类对象的属性;如何类对象属性没有@Trace修饰,修改类对象属性也不会引起 UI 刷新 当我们在组件内部直接修改变量本身时,则会提示Cannot assign to 'count' because it is a read-only property.

@Once

如果想要直接在组件内修改被@Param修饰的变量本身,可以配合@Once,但是,但是,但是,被@Once装饰的变量仅能在外部初始化一次,当在外部修改该变量本身时,不会同步到子组件。看下示例:

TypeScript 复制代码
@ObservedV2
class Person {
  @Trace name: string = ''
  @Trace age: number = 0
  @Trace address:Address = new Address()
}

@ObservedV2
class Address{
  @Trace zipCode : string = '000000'
  @Trace city:string = '北京'
}

两个数据类都被@ObservedV2修饰,属性都被@Trace修饰,方便我们观察数据变化。

TypeScript 复制代码
@ComponentV2
struct PersonView {
  @Require @Param person: Person = new Person()
  @Param count: number = 0

  build() {
    Column() {
      Text('子组件内容,未被@Once 修饰')
      Text(`${this.person.name}  ${this.person.age}`)
      Text(`${this.count}`).onClick((_) => {
        this.person.name += 'a'
        this.person.age++
      })
      Text(`${this.person.address.zipCode}  ${this.person.address.city}`)
    }
  }
}
@ComponentV2
struct PersonView2 {
  @Require @Param @Once person: Person = new Person()
  @Once @Param count: number = 0

  build() {
    Column() {
      Text('子组件内容,被@Once 修饰')
      Text(`${this.person.name}  ${this.person.age}`)
      Text(`${this.count}`).onClick((_) => {
        this.person.name += 'a'
        this.person.age++
      })
      Text(`${this.person.address.zipCode}  ${this.person.address.city}`)
    }
  }
}

两个自定义组件,其中 PersonView2内部的变量都是用@Once修饰。

TypeScript 复制代码
  @Local person: Person = new Person()
    build() {
      Column() {
        Text('父组件内容')
        Text(`${this.person.name}  ${this.person.age}`)
        Text(`${this.count}`)
        Text(`${this.person.address.zipCode}  ${this.person.address.city}`)

        PersonView({ person: this.person, count: this.count }).margin(10).backgroundColor("#66ace4")
        PersonView2({ person: this.person, count: this.count }).margin(10).backgroundColor("#66ace4")
        Flex({ wrap: FlexWrap.Wrap, space: { main: LengthMetrics.vp(5), cross: LengthMetrics.vp(5) } }) {
          Button('修改Person属性').onClick((_) => {
            this.person.age += 3
            this.person.name += 'c'
          })
          Button('修改Count').onClick((_) => {
            this.count += 7
          })
          Button('修改 address属性').onClick((_) => {
            this.person.address.zipCode += '9'
            this.person.address.city += 'w'
          })
          Button('对Person重新赋值').onClick((_) => {
            this.person = new Person()
            this.person.name = 'new person'
            this.person.age = 22
          })
        }
      }.margin(10).backgroundColor("#55ff6134")
    }

我们在父组件和两个子组件中传入了同一个person对象和count,然后用四个按钮来修改person对象的属性、修改person本身来做一个直观的展示

可以从图中看到,当我们点击修改Person属性按钮时,父组件、两个子组件都可以刷新 UI,而点击修改Count对Person重新赋值时,只有父组件和PersonView刷新了UI,可以看到@Once是拦截了数据源的变化,不影响@Param的观察能力。并且拦截的是数据源的赋值操作,修改其属性并不会被拦截。

还有一点就是,当我们点击对Person重新赋值后,再点击修改Person属性,发现只有父组件和PersonView刷新了UI,因为这时在父组件和PersonView中已经是新的对象了,而PersonView2中还是之前的对象,同步被打断,无法进行更新。

@Event

上面说了这么多,但就是想做到像 V1中的数据双向同步怎么办?毕竟这是一个挺常见的需求。 假如不提供对应装饰器的话,我们可以很容易想到让父组件传入一个回调方法,当需要改变数据时,调用传入的回调方法,让父组件来更新数据。 在 V2 中也是这么做的:使用@Event装饰器装饰回调方法并调用,可以实现更改数据源的变量,再通过@Local的同步机制,将修改同步回@Param,以此达到主动更新@Param装饰变量的效果。 文档上解释说是为了规范组件输出:

@Param标志着组件的输入,表明该变量受父组件影响,而@Event标志着组件的输出,可以通过该方法影响父组件。使用@Event装饰回调方法是一种规范,表明该回调作为自定义组件的输出。父组件需要判断是否提供对应方法用于子组件更改@Param变量的数据源。

直接看示例

TypeScript 复制代码
@ComponentV2
struct ShowEventAnno {
  @Event changeFontColor: () => void
  @Event changeFontSize: (size: number) => void
  @Require @Param fontColor: string
  @Require @Param fontSize: number

  build() {
    Column(){
      Text('ShowEventAnno View').fontColor(this.fontColor).fontSize(this.fontSize)
      Row(){
        Button('修改文字大小').onClick((_)=>{
          if(this.changeFontSize){
            this.changeFontSize(this.fontSize +2)
          }
        })
        Button('修改文字颜色').onClick((_)=>{
          if(this.changeFontColor){
            this.changeFontColor()
          }
        })
      }
    }
  }
}

使用@Event装饰箭头函数,这个函数的参数、返回值类型没有要求。当我们需要修改@Param修饰的变量时,比如在点击事件中,可以调用传入的@Event 修饰的函数,通知父控件对数据进行修改。

TypeScript 复制代码
  @Local fontColor: string = '#ff6134'
  @Local fontSize: number = 14
  build() {
      ShowEventAnno({
        fontColor: this.fontColor,
        fontSize: this.fontSize,
        changeFontColor: () => {
          if (this.fontColor == '#ff6134') {
            this.fontColor = '#39d167'
          } else {
            this.fontColor = '#ff6134'
          }
        },
        changeFontSize: (size: number) => {
          this.fontSize = size
        }
      })
  }

值得注意的是,使用@Event修改父组件的值是立刻生效的,但从父组件将变化同步回子组件的过程是异步的,即在调用完@Event的方法后,子组件内的值不会立刻变化。这是因为@Event将子组件值实际的变化能力交由父组件处理,在父组件实际决定如何处理后,将最终值在渲染之前同步回子组件。

@Monitor

可以用来监听状态变量的改变,在V1版本的@Watch中,无法实现对对象、数组中某一单个属性或数组项变化的监听,且无法获取变化之前的值。并且@Monitor既可以用在自定义组件中,也可以用在数据类中,可以同时监听多个对象属性,也可以精确的监听对象的某个属性。 需要注意的是:

  • 监听的变量需要被@Local、@Param、@Provider、@Consumer、@Computed装饰,未被状态变量装饰器装饰的变量在变化时无法被监听。@Monitor可以同时监听多个状态变量,这些变量名之间用","隔开。
  • 监听的状态变量为类对象时,仅能监听对象整体的变化。监听类属性的变化需要类属性被@Trace装饰,未被@Trace装饰的属性的变化无法被监听。
  • 支持对数组中的项进行监听,包括多维数组,对象数组。@Monitor无法监听内置类型(Array、Map、Date、Set)的API调用引起的变化。当@Monitor监听数组整体时,只能观测到数组整体的赋值。可以通过监听数组的长度变化来判断数组是否有插入、删除等变化。当前仅支持使用"."的方式表达深层属性、数组项的监听。

一个简单的例子:

TypeScript 复制代码
  @Local name: string = "xuan";
  @Local age: number = 12;
  @Monitor("message", "name")
  onStrChange(monitor: IMonitor) {
    monitor.dirty.forEach((paramsName: string) => {
      hilog.error(0x01, 'MonitorPage',
        `属性名:${paramsName}  由 ${monitor.value(paramsName)?.before} 修改为 ${monitor.value(paramsName)?.now}`)
    })
  }
  build() {
    Column() {
      Text(`name:${this.name}  message:${this.age}`)
      Button("修改简单类型变量")
        .onClick(() => {
          this.name += "a";
          this.age += 3
        })
    }
  }

当我们点击修改简单类型变量按钮时,会触发@Monitor装饰的方法,但需要注意的是,该方法的参数是IMonitor类型,它有两个属性: dirty: Array<string>value<T>(path?: string): IMonitorValue<T> | undefined,其中dirty保存发生变化的属性名。而value是一个方法,用于获取指定属性的变化信息,当该方法有返回值时,返回值是IMonitorValue类型,包含beforenowpath属性。

我们还可以精确的监听类对象的某个属性,这个监听可以写在类中,也可以写在控件中

TypeScript 复制代码
@ObservedV2
class Person {
  @Trace name: string = "Tom";
  @Trace region: string = "North";
  @Trace job: string = "Teacher";
  age: number = 25;

  // name被@Trace装饰,能够监听变化
  @Monitor("name")
  onNameChange(monitor: IMonitor) {
    hilog.error(0x01, 'MonitorTest', `name change from ${monitor.value()?.before} to ${monitor.value()?.now}`);
  }

  // age未被@Trace装饰,不能监听变化
  @Monitor("age")
  onAgeChange(monitor: IMonitor) {
    hilog.error(0x01, 'MonitorTest', `age change from ${monitor.value()?.before} to ${monitor.value()?.now}`);
  }

  // region与job均被@Trace装饰,能够监听变化
  @Monitor("region", "job")
  onChange(monitor: IMonitor) {
    monitor.dirty.forEach((path: string) => {
      hilog.error(0x01, 'MonitorTest',
        `${path} change from ${monitor.value(path)?.before} to ${monitor.value(path)?.now}`);
    })
  }
}

在控件中

TypeScript 复制代码
  @Local monitorTest: Person = new Person()
  @Monitor("monitorTest.name")
  onChangeName(monitor: IMonitor) {
    hilog.error(0x01, 'MonitorPage', '在自定义控件中监听对象属性变化:' +
      `${monitor.value()?.path} 由 ${monitor.value()?.before} 修改为 ${monitor.value()?.now}`)
  }
  build() {
Column() {
        Text(`name: ${this.monitorTest.name} ,age:${this.monitorTest.age} ,region: ${this.monitorTest.region}, job:${this.monitorTest.job}`)
          .padding(4)
          .borderWidth(1)
          .borderColor("#887612")


        Button("change name")
          .onClick(() => {
            this.monitorTest.name += 'b'; // 能够触发onNameChange方法
          })
        Button("change age")
          .onClick(() => {

            this.monitorTest.age += 1; // 不能够触发onAgeChange方法
          })
        Button("change region")
          .onClick(() => {
            this.monitorTest.region +='region '; // 能够触发onChange方法
          })
        Button("change job")
          .onClick(() => {
            this.monitorTest.job +='job '; // 能够触发onChange方法
          })
      }
  }

这个也有一点需要注意,当我们对类对象重新赋值,但是赋值前后类对象的属性值不变,也不会触发@Monitor回调。 还有一点需要注意的是监听的生效和失效时间: 当在组件中使用时,@Monitor会在状态变量初始化完成之后生效,并在组件销毁时失效。 当在类中使用时,@Monitor会在类创建完成后生效,在类销毁时失效。这个时机晚于类的constructor,早于自定义组件的aboutToAppear。然而由于类的实际销毁释放依赖于垃圾回收机制,因此会出现即使所在自定义组件已经销毁,类却还未及时销毁,导致类中定义的@Monitor仍在监听变化的情况。

@Computed

千呼万唤始出来的装饰器,谁家的状态管理还没有计算属性哇 该装饰器为方法装饰器,装饰getter方法。@Computed会检测被计算的属性变化,当被计算的属性变化时,@Computed只会被求解一次。 该装饰器既可以在组件中使用,也可以在数据类中使用。

TypeScript 复制代码
@ObservedV2
class Person{
  @Trace name:string =''
  @Trace age:number = 0

  @Computed
  get info(){
    hilog.error(0x01,'ComputedPge',"在数据类中获取所有信息");
    return `name:${this.name} , age:${this.age}`
  }
}
  @Local name: string = 'xuan';
  @Local age: number = 18;
  @Local address:string='北京'

  @Local person:Person = new Person()
  @Computed
  get info() {
    hilog.error(0x01,'ComputedPge',"获取所有信息");
    return `name:${this.name} ,age:${this.age} , address:${this.address}`
  }
  build() {
    Column() {

      Text(`${this.info}`)
      Text(`${this.info}`)
      Text(`${this.info}`)

      Button('修改简单属性').onClick(() => {
        this.age++;
      })

      Text(`${this.person.info}`)
      Text(`${this.person.info}`)
      Text(`${this.person.info}`)
      Button('修改对象属性').onClick(() => {
        this.person.name +='p'
      })
    }
  }

可以在控制台看到,虽然我们展示多次,但控制台只打印了一次信息;当我们分别点击修改简单属性修改对象属性时,控制台也是打印一次信息。 这里还有个注意点:@Computed装饰的属性可以初始化@Param

相关推荐
塞尔维亚大汉1 小时前
OpenHarmony(鸿蒙南向开发)——小型系统内核(LiteOS-A)【时间管理】
操作系统·harmonyos
喊我小垚女4 小时前
HarmonyOS第一课第四章习题答案
华为·harmonyos·鸿蒙·鸿蒙系统
HarmonyOS_SDK7 小时前
骨骼点检测技术详解:探索机器识别人体动作的奥秘
harmonyos
谢道韫66611 小时前
从0到1:ArkTS实现鸿蒙策略模式全解析
华为·harmonyos·策略模式
Android技术之家13 小时前
如何对比Android组件快速学习鸿蒙Next开发
android·学习·华为·harmonyos
winfredzhang13 小时前
如何在华为harmonyOS上调试软件
adb·harmonyos·授权·开发者模式
da_caoyuan1 天前
【HarmonyOS Next 自定义可拖拽image】
华为·harmonyos·可拖动·鸿蒙next
liuhaikang1 天前
【鸿蒙HarmonyOS Next实战开发】mp4parser库-音视频裁剪、合成、取帧等操作
harmonyos
liuhaikang1 天前
【鸿蒙HarmonyOS Next实战开发】多媒体视频播放-GSYVideoPlayer
华为·音视频·harmonyos