@ObservedV2和@Trace

使用目的

为了增强状态管理框架对类对象中属性的观测能力
使用之后嵌套对象内属性变化可直接引起UI刷新

注意事项

  • 两个装饰器需要配合使用,单一使用无效
  • 被Trace装饰的属性变化时,仅会通知属性关联的组件进行刷新
  • 使用这两种装饰器的类,需要通过new实例化之后,才具备被观测的能力
  • 不可与@State等V1装饰器混用

实践

class Son{
  name:string =  '张三'
}
class Father{
  son: Son = new Son()
}

@Entry
struct Index{
  @State param: Father = new Father()
  build() {
    Column(){
      Text(this.param.son.name)
      Button('change')
        .onClick(()=>{
          this.param.son.name = '李四'
        })
    }
  }
}

在V1中,直接对嵌套对象的属性进行修改,@State是观测不到的,虽然在V1中使用@ObjectLink和@Observed可以进行观测,但是当嵌套层级过深时,代码会变得十分复杂,每个嵌套的属性类都需要创建一个新的组件,单独用@ObjectLink修饰

@ObservedV2
class Son{
  @Trace name:string =  '张三'
}
@ObservedV2
class Father{
  son: Son = new Son()
}

@Entry
@ComponentV2
struct Index{
  param: Father = new Father()
  build() {
    Column(){
      Text(this.param.son.name)
      Button('change')
        .onClick(()=>{
          this.param.son.name = '李四'
        })
    }
  }
}

使用@ObservedV2和@Trace就解决这个问题,在开发过程中不再需要去修改UI部分,只需要在类型定义部分使用@Trace装饰需要渲染的属性即可

@ComponentV2

使用目的

为了让自定义组件能够使用状态管理V2的能力

注意事项

  • 在该装饰器装饰的自定义组件中,能且仅能使用状态管理V2
  • 暂不支持LocalStorage等原本@Component支持的能力

类比V1中的@Component,就是能让自定义组件也能使用状态管理V2的能力

@Local

使用目的

实现@ComponentV2装饰的自定义组件中变量变化的检测

注意事项

  • 被装饰的变量无法从外部初始化,必须在组件内部初始化
  • @Local装饰简单类型是,能观测到变量值的赋值;装饰对象类型时,仅能观测到对象整体的赋值;装饰数组类型时,能够观测到数组整体以及数组元素项的变化;装饰Array、Set、Map等类型内嵌类型,可以观测通过API调用带来的变化
  • @Local支持null、undefined以及联合类型

设计目的

使用@State装饰的状态变量可以从外部初始化,因此无法保证@State装饰的变量初始值一定为组件内部定义的,不利于状态管理,而@Local就解决了这个问题

和@State的区别

@State不支持深度观测,而@Local则依赖于@Trace装饰器

@Param

使用目的

为了增强子组件接受外部参数的能力
@Param装饰的变量不仅可以接受组件外部的输入,还可以接受@Local的同步变化

注意事项

  • @Param装饰的变量支持本地初始化,但不允许在组件内部直接修改
  • 被装饰的变量支持初始化自定义时从外部传入,当传入的数据也为状态变量时,修改会同步给@Param
  • 对于复杂对象,@Param获得的是数据源的引用。在组件中可以修改类对象的属性,修改会同步给数据源
  • 对于复杂对象,@Param不允许修改引用本身,对于简单对象不允许修改值本身

设计目的

在状态管理V1中存在多种可接受外部传入的装饰器,这些装饰器使用各有限制,这些限制也让组件之间的传值规则变得非常复杂,不易使用。因此推出@Trace装饰器,就像是一个统一的接口,表示组件从外部传入的状态。

实践

@ObservedV2
class Son {
  @Trace name: string = '张三'
}

@ObservedV2
class Father {
  son: Son = new Son()
}

@Entry
@ComponentV2
struct Index {
  @Local param: Father = new Father()

  build() {
    Column() {
      Text('parent:' + this.param.son.name)
      child({ param: this.param.son })
    }
  }
}

@ComponentV2
struct child {
  @Param param: Son = new Son()

  build() {
    Column() {
      Text('child:' + this.param.name)
      Button('change')
        .onClick(() => {
          this.param.name = '李四'
        })
    }
  }
}

由于@Param获得的是数据源的引用,所以在子组件中修改数据源的值是可以同步回父组件的
但是若保持代码结构不变,仅将V2的装饰器替换为V1的@State和@Link,则会产生运行时报错,程序闪退

@Link渲染更新流程

  1. 初始渲染:执行父组件的build函数,创建子组件的实例
    • 指定父组件中的@State变量用于初始化子组件的@Link变量
    • 父组件的状态变量包装类通过子组件的构造函数传递给子组件。子组件的@Link包装类拿到父组件的状态变量后,会将当前@Link包装类实例注册给父组件的@State变量

    这里的注册意思就是,让父组件的@State包装类持有@Link的this,告诉父组件子组件的 “某某某” 正在引用它

  2. 数据源变化引起的@Link更新
    • 在第一步中,子组件的@Link包装类把当前this指针注册给父组件。父组件的@State变更后,会遍历更新所有依赖它的系统组件和状态变量
    • 通知@Link包装类更新后,子组件中依赖@Link的组件也会被更新,从而实现父->子的更新
  3. 在子组件内更改@Link的值
    • @Link被修改之后,会调用父组件的@State包装类的set方法,将数据同步回父组件
    • 父组件内@State的值修改后又会像第二步一样,遍历刷新所有的依赖的组件,实现子->父的更新

在上述的例子中,若是给@Link子组件传入一个@State的属性,就相当于是给@Link传入了一个非状态变量,@lLink无法进行注册,也就产生一些无法预知的错误

@Once

使用目的

为了实现仅从外部初始化一次且不接受后续同步变化的能力

注意事项

  • @Once必须搭配@Param使用
  • @Once不会影响@Param的观测能力,仅对数据源的变化做出拦截
  • @Once与@Param搭配使用时,可以在本地修改@Param的值,且能够触发UI刷新
  • @Once本身不具有观察能力

实践

@ObservedV2
class Son {
  @Trace name: string = '张三'
}

@ObservedV2
class Father {
  son: Son = new Son()
}

@Entry
@ComponentV2
struct Index {
  @Local param: Father = new Father()

  build() {
    Column() {
      Text('parent:' + this.param.son.name)
      Button('change')
        .onClick(() => {
          this.param.son.name = '李四'
        })
      child({ param: this.param })
    }
    .size({width:'100%',height:'100%'})
  }
}

@ComponentV2
struct child {
  @Param @Once param: Father = new Father()

  build() {
    Column() {
      Text('child:' + this.param.son.name)
      Button('change')
        .onClick(() => {
          this.param = new Father()
        })
    }
  }
}

使用@Once可以在子组件内修改@Param的值,有点类似于?@Local,不过不同的是可以接受外部赋值

@Event

使用目的

为了实现子组件与父组件之间的双向通信能力
即子组件想要修改时,不会在子组件内进行修改,而是向父组件发出修改请求,修改后再同步回自身,实现双向同步

注意事项

  • @Event装饰的回调方法中的参数以及返回值由开发者决定
  • @Event装饰非回调类型的变量时不会生效。当@Event没有初始化时,会自动生成一个空函数作为默认回调
  • 调用完@Event的方法后子组件内的值不会立即变化,因为子组件的变化能力是交由父组件处理的,具体要看父组件内的处理细节,而且父组件将变化同步回子组件的过程也是异步的

有点类似于@Once,前者是作为一个辅助装饰器单拎出来,@Event则是像另一一个辅助装饰器与@Param的封装体,也可以接受外部赋值

实践

@Entry
@ComponentV2
struct Index {
  @Local param: string = '张三'

  build() {
    Column() {
      child({
        param: this.param,
        onFun: (param: number) => {
          if(param === 1){
            this.param = '李四'
          }else{
            this.param = '王五'
          }
        }
      })
    }
    .size({width:'100%',height:'100%'})
  }
}

@ComponentV2
struct child {
  @Param param:string = ''
  @Event onFun: (param: number)=>void = (param: number) =>{}

  build() {
    Column() {
      Text('child: '+this.param)
      Button('change1')
        .onClick(() => {
          this.onFun(1)
        })
      Button('change2')
        .onClick(() => {
          this.onFun(2)
        })
    }
  }
}

V2中简单化了实现父子间双向同步的逻辑,不直接操作子组件内的状态变量,而是通过一个箭头函数来修改父组件中的状态变量,让开发者可以自定义子->父更新的数据相应逻辑,自由度更高

@Provider和@Consumer

与V1中的@Provide对比

  • V2中的Provider支持function类型,V1不支持
  • V2中的Provider使用时后面括号内的值(aliasName)是唯一匹配的key,而V1中的aliasName和属性名都为key,会优先匹配alias
  • V2中的Provider不支持从父组件初始化,V1支持
  • V2中的@Provider支持重名,V1不支持,若想在V1中使用(重载),则需额外配置

注意事项

  • 若不给Provider配置aliasName,则会使用属性名作为默认aliasName
  • @Consumer会向上查找其最近的父@Provider的数据
  • 若@Consumer未找到相应的@Provider,则会变为类似于@Local装饰器单独使用

实践

@Entry
@ComponentV2
struct Index {
  @Provider('key') param: string = '0'
  build() {
    Column() {
      child1()
    }
    .size({width:'100%',height:'100%'})
  }
}

@ComponentV2
struct child1 {
  @Provider('key') param:string = '1'
  build() {
    Column() {
      leaf()
    }
  }
}
@ComponentV2
struct leaf{
  @Consumer('key') param: string = ''
  build() {
    Text(this.param)
  }
}

若@Provider的别名相同,则向上查找最近的@Provider,若不存在,需要注意的是@Consumer只会向上查找父组件,不会查找兄弟组件

@Monitor

使用目的

为了增强状态管理框架对状态变量变化的监听能力
@Monitor装饰器用于监听状态变量的修改,使得状态变量具有深度监听的能力

注意事项

  • 该装饰器支持在类中与@ObserbedV2、@Trace配合使用,实现对类内属性的变化监听
  • 判断是否属性变化时,@Monitor使用的是严格相等(===)
  • 单个@Monitor能够同时监听多个属性的变化,当这些属性在一此事件中共同变化时,只会触发一次变化回调
  • 当@Monitor监听整个数组时,数组单个项的更改、内置属性的api调用,都无法被监听
  • 在继承类场景中,若在父子组件中对同一属性分别设置@Monitor监听,当属性变化时,两个Monitor回调均会被调用
  • 当@Monitor传入多个路径参数时,会将参数全部拼成一个字符串,并加上空格,一次来判断是否重复监听
  • 若向@Monitor的路径传入一个变量,则以初始变量的值为准,后续改变变量的值不会改变Monitor的监听对象
  • 当@Monitor监听的变量由可访问状态变为不可访问状态时,不会触发监听
let a = new A()//可访问
a = null//不可访问
a = undefined//不可访问

设计目的

状态管理V1无法实现对对象、数组中某个属性或者数组项变化的监听,也无法获取变化前的值,因此推出@Monitor装饰器

@Monitor与@Watch的区别

@Watch@Monitor
参数回调方法名监听目标的名称
监听能力T 仅能监听到引用是否改变可以监听到深层变化
监听目标数量一个多个
能否获取变化前的值不能

实践

@Entry
@ComponentV2
struct Index {
  @Local before: string = 'default'
  @Local after: string = 'default'
  @Provider('key') param: Father = new Father()
  @Monitor('param.son.name')
  paramChange(monitor: IMonitor){
    this.before = monitor.value()?.before as string
    this.after = monitor.value()?.now as string
  }
  build() {
    Column() {
      Text(`before:${this.before} after: ${this.after}`)
      Button('change')
        .onClick(()=>{
          this.param.son.name = '李四'
        })
    }
    .size({width:'100%',height:'100%'})
  }
}

@Monitor不仅可以监听到复杂对象的变化,而且可以获得变量变化前后的值,不过@Monitor的入参写法比较特殊,需要注意

@Computed

使用目的

当给UI绑定了相同的计算逻辑时,为了节约多次重复计算导致的性能问题,可以使用@Computed来计算属性

注意事项

  • 不能在被装饰的getter方法中修改参与计算的属性,否则会一致循环计算导致应用冻结
  • 被装饰的方法仅在初始化,或者计算的状态变量变化时,才会触发计算
  • 被装饰的属性为只读,无法与双向绑定配合使用
  • 计算属性本身会带来性能开销,对于简单的计算逻辑,可以不使用计算属性
  • 计算属性可以初始化@Param

实践

@Entry
@ComponentV2
struct Index {
  @Local param1: string = 'param1'
  @Local param2: string = 'param2'
  build() {
    Column() {
      Text(this.param1 + this.param2)
      Text(this.param1 + this.param2)
    }
    .size({width:'100%',height:'100%'})
  }
}

在上述场景中,系统组件的入参绑定了一个计算表达式,当需要用到多次相同组件时,每次都会重复计算,造成资源浪费,而使用@Computed即可解决这个问题

@Entry
@ComponentV2
struct Index {
  @Local param1: string = 'param1'
  @Local param2: string = 'param2'
  @Computed
  get getParam(){
    return this.param1 + this.param2
  }
  build() {
    Column() {
      Text(this.getParam)
      Text(this.getParam)
    }
    .size({width:'100%',height:'100%'})
  }
}

@Computed结合get关键字即可避免重复计算,初始化时只会计算一次,并且将计算结果保存至缓存中,后需getter方法相关的状态变量变化时才会重新计算

@Type

使用目的

在持久化一个class时,为了实现序列化类时不丢失属性的复杂类型,可以使用@Type来装饰类属性

使用限制

  • 不支持collection.Set、collection.Map等类型
  • 不支持简单类型
  • 不支持构造函数含参数的类

@ReusableV2

使用目的

为了降低反复创建销毁自定义组件时带来的性能开销,开发者可以使用@ReusableV2来装饰自定义组件

注意事项

  • @ReusableV2同样提供了aboutToRecycle和abouotToReuse回调,但是与@Reusable不同的是,V2中的abouotToReuse(复用回调)没有入参
  • 在回收阶段,框架会递归地调用所有子组件的Recycle回调;在复用阶段,则会递归地调用所有子组件的Reuse回调(即使组件未被@ReusableV2修饰)

和V1的区别

  • 在V1中,组件进入复用池后仍会响应更新,带来不必要的性能消耗,需要手动使用组件冻结能力才能解决;V2在组件进入复用池时会自动冻结组件,冻结状态将持久到Reuse回调,在此之前(不包括Reuse回调)的状态变量修改都不会触发UI变化
  • V2在复用之前会重置组件中的状态变量以及相关的@Computed@Monitor的内容。在复用过程中,所有V2的自定义组件都会重置,无论是否被标记

实践

@Entry
@ComponentV2
struct Index {
  @Local data: MyDataSource = new MyDataSource()
  aboutToAppear(): void {
    for (let i = 0; i <= 200; i++) {
      this.data.pushData(new NumberData(i));
    }
  }
  build() {
    List() {
     LazyForEach(this.data,(item: NumberData) => {
       ListItem(){
         child({item: item})
       }
     })
    }
    .size({width:'100%',height:'100%'})
    .cachedCount(5)
  }
}
@ReusableV2
@ComponentV2
struct child{
  @Param item: NumberData = new NumberData(-1)
  aboutToAppear(): void {
    console.log(`组件${this.item.index}已经创建`)
  }
  aboutToReuse(): void {
    console.log(`组件${this.item.index}已经复用`)
  }
  aboutToRecycle(): void {
    console.log(`组件${this.item.index}已经回收`)
  }
  build() {
    Column(){
      Text(`组件${this.item.index}`)
        .fontSize(24)
    }
    .width('100%')
    .height(100)
  }
}

上述代码中,组件19的创建就是复用了缓存池中的组件,此时组件0被回收
在这里插入图片描述

Logo

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

更多推荐