状态管理V1

3.1 状态管理概述

在声明式UI编程框架中,UI是程序状态的运行结果,用户构建了一个UI模型,其中应用的运行时的状态是参数。当参数改变时,UI作为返回结果,也将进行对应的改变。这些运行时的状态变化所带来的UI的重新渲染,在ArkUI中统称为状态管理机制。

自定义组件拥有变量,变量必须被装饰器装饰才可以成为状态变量,状态变量的改变会引起UI的渲染刷新。如果不使用状态变量,UI只能在初始化时渲染,后续将不会再刷新。

  • View(UI):UI渲染,指将build方法内的UI描述和@Builder装饰的方法内的UI描述映射到界面。

  • State:状态,指驱动UI更新的数据。用户通过触发组件的事件方法,改变状态数据。状态数据的改变,引起UI的重新渲染。

3.1.1 基本概念

在这里插入图片描述

  1. 状态变量:被状态装饰器装饰的变量,状态变量值的改变会引起UI的渲染更新。示例:@State num: number = 1,其中,@State是状态装饰器,num是状态变量。还有其他的装饰器也可以装饰状态变量
    管理组件拥有的状态,即图中Components级别的状态管理:
  • @State:@State装饰的变量拥有其所属组件的状态,可以作为其子组件单向和双向同步的数据源。当其数值改变时,会引起相关组件的渲染刷新。

  • @Prop:@Prop装饰的变量可以和父组件建立单向同步关系,@Prop装饰的变量是可变的,但修改不会同步回父组件。

  • @Link:@Link装饰的变量可以和父组件建立双向同步关系,子组件中@Link装饰变量的修改会同步给父组件中建立双向数据绑定的数据源,父组件的更新也会同步给@Link装饰的变量。

  • @Provide/@Consume:@Provide/@Consume装饰的变量用于跨组件层级(多层组件)同步状态变量,可以不需要通过参数命名机制传递,通过alias(别名)或者属性名绑定。

  • @Observed:@Observed装饰class,需要观察多层嵌套场景的class需要被
    @Observed装饰。单独使用@Observed没有任何作用,需要和@ObjectLink、@Prop联用。

  • @ObjectLink:@ObjectLink装饰的变量接收@Observed装饰的class的实例,应用于观察多层嵌套场景,和父组件的数据源构建双向同步。

  1. 常规变量:没有被状态装饰器装饰的变量,通常应用于辅助计算。它的改变永远不会引起UI的刷新。
  2. 数据源/同步源:状态变量的原始来源,可以同步给不同的状态数据。通常意义为父组件传给子组件的数据。
  3. 命名参数机制:父组件通过指定参数传递给子组件的状态变量,为父子传递同步参数的主要手段。
  4. 从父组件初始化:父组件使用命名参数机制,将指定参数传递给子组件。子组件初始化的默认值在有父组件传值的情况下,会被覆盖。
@Component
struct MyComponent {
  @State count: number = 0;
  private increaseBy: number = 1;
 
  build() {
  }
}
 
@Component
struct Parent {
  build() {
    Column() {
      // 从父组件初始化,覆盖本地定义的默认值
      MyComponent({ count: 1, increaseBy: 2 })
    }
  }
}
3.2 管理组件拥有的状态
3.2.1 @State装饰器-组件内状态

@State装饰的变量,或称为状态变量,一旦变量拥有了状态属性,就和自定义组件的渲染绑定起来。当状态改变时,UI会发生对应的渲染改变。

在状态变量相关装饰器中,@State是最基础的,使变量拥有状态属性的装饰器,它也是大部分状态变量的数据源。

1. 概述

@State装饰的变量,与声明式范式中的其他被装饰变量一样,是私有的,只能从组件内部访问(在组件外部可以选择传参进去),在声明时必须指定其类型和本地初始化。初始化也可选择使用命名参数机制从父组件完成初始化。

@State装饰的变量拥有以下特点:

  • @State装饰的变量与子组件中的@Prop、@Link或@ObjectLink装饰变量之间建立单向或双向数据同步。
  • @State装饰的变量生命周期与其所属自定义组件的生命周期相同。
2. 使用规则说明

允许装饰的变量类型

  • Object、class、string、number、boolean、enum(枚举)类型,以及这些类型的数组。类型必须被指定。

  • 不支持any,不支持简单类型和复杂类型的联合类型,不允许使用undefined和null。

    ​ 说明:建议不要装饰Date类型,应用可能会产生异常行为。

  • 不支持Length、ResourceStr、ResourceColor类型,Length、ResourceStr、ResourceColor为简单类型和复杂类型的联合类型。

被装饰变量的初始值必须本地初始化

3. 使用场景

A. 装饰简单类型的变量

以下示例为@State装饰的简单类型,count被@State装饰成为状态变量,count的改变引起Button组件的刷新:

  • 当状态变量count改变时,查询到只有Button组件关联了它;
  • 执行Button组件的更新方法,实现按需刷新。
//自定义类型
class Person{
  public name:string
  public age:number

  constructor(xName:string,xAge:number) {

    this.name = xName
    this.age = xAge
  }
}

//定义子组件
@Component
struct MyChild{
  @State
  person: Person = new Person('高木',15)//状态变量
  private increase: number = 1//常规变量

  build(){
    Column(){
      Text(`${this.person.name}`).height(80)
        Divider()
      Button(`修改名字`).onClick(()=>{
        this.person.name = this.person.name === '高木'?'小天才': '高木'//如果是高木就换成小天才,如果是小天才就换成高木
      }).height(80).width(250).margin(5)

      Button(`修改年龄:${this.person.age}`).onClick(()=>{
        this.person.age += this.increase
      }).height(80).width(250).margin(5)
    }
  }
}

//定义一个入口页面
@Entry
@Component
struct MyParent{
  build(){
    Column(){
      //构建两个子组件
      MyChild({person: new Person('小天才',18)})
      MyChild({increase: 2})
    }
  }
}

注意

两个子组件是并行的,改变其中一个子组件不会影响另一个子组件的运行,但每一个子组件和上面的父组件有关系

3.2.2 @Prop装饰器-父子单向同步

@Prop装饰的变量可以和父组件建立单向的同步关系。@Prop装饰的变量是可变的,但是变化不会同步回其父组件。

1. 概述

@Prop装饰的变量和父组件建立单向的同步关系:

A. @Prop变量允许在本地修改,但修改后的变化不会同步回父组件。

B. 当父组件中的数据源更改时,与之相关的@Prop装饰的变量都会自动更新。如果子组件已经在本地修改了@Prop装饰的相关变量值,而在父组件中对应的@State装饰的变量被修改后,子组件本地修改的@Prop装饰的相关变量值将被覆盖。

2. 限制条件

A. @Prop修饰复杂类型时是深拷贝,在拷贝的过程中除了基本类型、Map、Set、Date、Array外,都会丢失类型。

B. @Prop装饰器不能在@Entry装饰的自定义组件中使用。

3. 使用规则说明
@Prop变量装饰器 说明
装饰器参数
同步类型 单向同步:对父组件状态变量值的修改,将同步给子组件@Prop装饰的变量,子组件@Prop变量的修改不会同步到父组件的状态变量上
允许装饰的变量类型 string、number、boolean、enum类型。不支持any,不允许使用undefined和null。必须指定类型。在父组件中,传递给@Prop装饰的值不能为undefined或者null,反例如下所示。CompA ({ aProp: undefined })CompA ({ aProp: null })@Prop和数据源类型需要相同,有以下三种情况(数据源以@State为例):@Prop装饰的变量和父组件状态变量类型相同,即@Prop : S和@State : S;当父组件的状态变量为数组时,@Prop装饰的变量和父组件状态变量的数组项类型相同,即@Prop : S和@State : Array;当父组件状态变量为Object或者class时,@Prop装饰的变量和父组件状态变量的属性类型相同,即@Prop : S和@State : { propA: S }。
被装饰变量的初始值 允许本地初始化。
4. 变量的传递/访问规则说明
传递/访问 说明
从父组件初始化 如果本地有初始化,则是可选的。没有的话,则必选,支持父组件中的常规变量、@State、@Link、@Prop、@Provide、@Consume、@ObjectLink、@StorageLink、@StorageProp、@LocalStorageLink和@LocalStorageProp去初始化子组件中的@Prop变量。
用于初始化子组件 @Prop支持去初始化子组件中的常规变量、@State、@Link、@Prop、@Provide。
是否支持组件外访问 @Prop装饰的变量是私有的,只能在组件内访问。
5. 使用场景

A. 父组件@State到子组件@Prop简单数据类型同步

//定义子组件
@Component
struct MyChild{

  @Prop
  age: number//状态变量

  private increase : number = 1//常规变量

  build(){
    Column(){
      if (this.age >=18 ){
        Text(`子组件中的age已经成年了: ${this.age}`).height(80)
      }
      else{
        Text(`子组件未成年: ${this.age}`).height(80)
      }

      Button('-修改子组件age').onClick(()=>{
        this.age -= this.increase
      }).height(80).width(250).margin(5)
    }
  }
}


@Entry
@Component
struct MyParent{
  @State
  init_age:number = 16
  build(){

    Column(){
      Text(`父组件中的初始age为:${this.init_age}`).height(80)

      Button('+修改父组件的init_age').onClick(()=>{
        this.init_age += 1
      }).height(80).width(250).margin(5)

      Divider()

      MyChild({age:this.init_age,increase: 2})
    }
  }
}

注意

改变父组件中的init_age,因为init_age是状态变量,所以父组件的改变也将影响子组件中的age。

但在执行子组件减法运算时,会刷新子组件相对应的UI。并不会影响到父组件中的init_age以至于影响UI。

当再一次进行父组件的加法运算时,又会同时更新父子组件的init_age和age并同时更新UI。

B. 父组件@State数组项到子组件@Prop简单数据类型同步

父组件中@State如果装饰的数组,其数组项也可以初始化@Prop。

 //定义子组件
@Component
struct MyChild{

  @Prop
  age: number = 12//状态变量,进行本地初始化

  private increase : number = 1//常规变量

  build(){
    Column(){
      if (this.age >=18 ){
        Text(`子组件中的age已经成年了: ${this.age}`).height(80)
      }
      else{
        Text(`子组件未成年: ${this.age}`).height(80)
      }

      Button('-修改子组件age').onClick(()=>{
        this.age -= this.increase
      }).height(80).width(250).margin(5)
    }
  }
}


@Entry
@Component
struct MyParent{
  @State
  init_age:number = 16
  build(){

    Column(){
      Text(`父组件中的初始age为:${this.init_age}`).height(80)

      Button('+修改父组件的init_age').onClick(()=>{
        this.init_age += 1
      }).height(80).width(250).margin(5)

      Divider()

      MyChild({age:this.init_age,increase: 2})
      Divider()
      MyChild({increase: 2})
    }
  }
}

注意

因为第二个子组件并没有传入init_age,所以父组件 的改变不会影响该子组件的UI刷新,但子组件内部可以更改自己的age以刷新UI。

而因为没有传入父组件中的值,则默认启用本地初识化的值即第六行进行的本地初始化为12

3.2.3 @Link装饰器-父子双向同步

子组件中被@Link装饰的变量与其父组件中对应的数据源建立双向数据绑定。

注意

@Link装饰的变量与其父组件中的数据源共享相同的值。@Link装饰器不能在@Entry装饰的自定义组件中使用。

1. 使用规则说明
@Link变量装饰器 说明
装饰器参数
同步类型 双向同步。父组件中@State, @StorageLink和@Link 和子组件@Link可以建立双向数据同步,反之亦然。
允许装饰的变量类型 Object、class、string、number、boolean、enum类型,以及这些类型的数组。类型必须被指定,且和双向绑定状态变量的类型相同。不支持any,不支持简单类型和复杂类型的联合类型,不允许使用undefined和null。说明:不支持Length、ResourceStr、ResourceColor类型,Length、ResourceStr、ResourceColor为简单类型和复杂类型的联合类型。
被装饰变量的初始值 无,禁止本地初始化。
2. 变量的传递/访问规则说明
传递/访问 说明
从父组件初始化和更新 必选。与父组件@State, @StorageLink和@Link 建立双向绑定。允许父组件中@State、@Link、@Prop、@Provide、@Consume、@ObjectLink、@StorageLink、@StorageProp、@LocalStorageLink和@LocalStorageProp装饰变量初始化子组件@Link。从API version 9开始,@Link子组件从父组件初始化@State的语法为Comp({ aLink: this.aState })。同样Comp({aLink: $aState})也支持。
用于初始化子组件 允许,可用于初始化常规变量、@State、@Link、@Prop、@Provide。
是否支持组件外访问 私有,只能在所属组件内访问。
3. 实例
//自定义按钮的信息类
class ButtonState{

  value: string
  width: number = 0

  constructor(value:string,width:number) {
    this.value = value
    this.width = width
  }
}


@Component
struct MyChildeGreenButton{
  //拥有绿色按钮的组件,Link装饰器实现双向同步
  @Link
  buttonState: ButtonState;
  build(){
    Button(`${this.buttonState.value}`)
      .width(this.buttonState.width)
      .height(150)
      .backgroundColor(Color.Green)
      .onClick(()=>{
        //点击按钮,实现宽度的变化
        if(this.buttonState.width<700){
          this.buttonState.width += 100
        }
        else{
          //按钮的宽度回到初始值
          this.buttonState = new ButtonState(`绿色按钮`,100)
        }
      })
  }
}

@Component
struct MyChildeRedButton{
  //拥有红色按钮的组件,Link装饰器实现双向同步
  @Link
  value: string;
  @Link
  Buttonwidth: number;

  build(){
    Button(`${this.value}`)
      .width(this.Buttonwidth)
      .height(150)
      .backgroundColor(Color.Red)
      .onClick(()=>{
        //点击按钮,实现宽度的变化
        this.Buttonwidth += 50
      })
  }
}

@Entry
@Component
struct MyParent{

  @State parentGreenButton: ButtonState = new ButtonState('一号子组件',300)
  @State parentRedValue: string = '二号子组件'
  @State parentRedWidth: number = 200//红色按钮的宽度

  build(){
    Column(){
      //父组件中调整的宽度
      Button(`父组件中修改绿色按钮的宽度:${this.parentGreenButton.width}`)
        .onClick(()=>{
          this.parentGreenButton.width = this.parentGreenButton.width <700 ? this.parentGreenButton.width + 100 : 100
        })

      Button(`父组件中修改红色按钮的宽度:${this.parentRedWidth}`)
        .onClick(()=>{
          this.parentRedWidth = this.parentRedWidth <700 ? this.parentRedWidth + 100: 100
        })

      Divider()
      MyChildeGreenButton({buttonState: $parentGreenButton})
      MyChildeRedButton({value:$parentRedValue,Buttonwidth:$parentRedWidth})
    }
  }
}
3.3 应用程序状态
  • 通过组件的状态数据可以实现组件之间的数据绑定,实现父组件和子组件之间的互动,比较适合用于单个页面内多个组件的情景。对于多个页面之间的数据共享采用应用存储(AppStorage)更为合适。
  • AppStorage是应用程序中的单例对象,由UI框架在应用程序启动时创建,在应用程序退出时销毁,为应用程序范围内的可变状态属性提供中央存储。AppStorage可以保存属性及属性值,属性值可以通过唯一的键值进行访问。
  • UI组件可以通过装饰器与AppStorage进行同步,应用业务逻辑的实现也可以通过接口访问AppStorage存储的数据。
  • 组件通过使用@StorageLink(key)装饰的状态变量,与AppStorage建立双向数据绑定。
  • 组件通过使用@StorageProp(key)装饰的状态变量,与AppStorage建立单向数据绑定。

AppStorage中存储的数据采用的是键值对的形式,AppStorage提供了操作其存储数据的接口API,常用的API如表所示

方法声明 说明
Set(key:string,newValue:T):void 设置已保存的key值为新的newValue值
SetOrCreate(key:string,newValue:T):boolean 创建一个键为key,值为newValue的属性,如果可以已存在,且可以改写则替换为新值newValue,返回true,否则返回false。属性值不支持null和undefined
Get(key:string):T 获取key对应的值,如果不存在则返回undefined
Has(propName:string):boolean 判断对应键值的属性是否存在
Link(key:string):@Link 如果存在具有给定键的数据,则返回到此属性的双向数据绑定,该双向绑定意味着变量或者组件对数据的更改将同步到AppStorage,通过AppStorage对数据的修改将同步到变量或者组件。如果具有此键的属性不存在或属性为只读,则返回undefined。
SetAndLink(key:string,defaultValue:T):@Link 与Link接口类似,不同的是,在key不存在时创建并赋默认值
Prop(key:string):@Link 与Link不同的是,该接口建立的是单向绑定。Prop方法对应的属性值类型为只能是简单类型
SetAndProp(propName:string,defaultValue:S):@Prop 与Prop接口类似,不同的是,在key不存在时创建并赋默认值
Keys():array 返回包含所有键的字符串数组
Delete(key:string):boolean 删除key对应的键值对
IsMutable(key:string):boolean 判断key属性是否存在且可以改变
Clear():boolean 删除所有的属性,当前有状态变量依旧引用属性,则不能清除,返回false

另外,系统框架提供的环境对象(Environment)也是在应用程序启动时创建的单例对象,它可以为AppStorage提供一些列应用程序需要的环境状态属性。

Environment.EnvProp("accessibilityEnabled", "default")

以上代码可以使无障碍屏幕朗读环境变量和AppStorage建立联系,通过AppStorage可以访问该变量。

var read = AppStorage.Get("accessilibiltEnabled")  //获得环境变量值
判断key属性是否存在且可以改变                                |
| Clear():boolean                                  | 删除所有的属性,当前有状态变量依旧引用属性,则不能清除,返回false |

 另外,系统框架提供的环境对象(Environment)也是在应用程序启动时创建的单例对象,它可以为AppStorage提供一些列应用程序需要的环境状态属性。

```TypeScript
Environment.EnvProp("accessibilityEnabled", "default")

以上代码可以使无障碍屏幕朗读环境变量和AppStorage建立联系,通过AppStorage可以访问该变量。

var read = AppStorage.Get("accessilibiltEnabled")  //获得环境变量值

状态管理V2

状态管理V1现状以及V2优点

状态管理V1使用代理观察数据,当创建一个状态变量时,同时也创建了一个数据代理观察者。该观察者可感知代理变化,但无法感知实际数据变化,因此在使用上有如下限制:

状态变量不能独立于UI存在,同一个数据被多个视图代理时,在其中一个视图的更改不会通知其他视图更新。
只能感知对象属性第一层的变化,无法做到深度观测和深度监听。
在更改对象中属性以及更改数组中元素的场景下存在冗余更新的问题。
装饰器间配合使用限制多,不易用。组件中没有明确状态变量的输入与输出,不利于组件化。
在这里插入图片描述
状态管理V2将观察能力增强到数据本身,数据本身就是可观察的,更改数据会触发相应的视图的更新。相较于状态管理V1,状态管理V2有如下优点:

状态变量独立于UI,更改数据会触发相应视图的更新。

支持对象的深度观测和深度监听,且深度观测机制不影响观测性能。

支持对象中属性级精准更新及数组中元素的最小化更新。

装饰器易用性高、拓展性强,在组件中明确输入与输出,有利于组件化。
在这里插入图片描述
装饰器总览
状态管理(V2试用版)提供了一套全新的装饰器。

  • @ObservedV2:@ObservedV2装饰器装饰class,使得被装饰的class具有深度监听的能力。@ObservedV2和@Trace配合使用可以使class中的属性具有深度观测的能力。

  • @Trace:@Trace装饰器装饰被@ObservedV2装饰的class中的属性,被装饰的属性具有深度观测的能力。

  • @ComponentV2:使用@ComponentV2装饰的struct中能使用新的装饰器。例如:@Local、@Param、@Event、@Once、@Monitor、@Provider、@Consumer。

  • @Local:@Local装饰的变量为组件内部状态,无法从外部初始化。

  • @Param:@Param装饰的变量作为组件的输入,可以接受从外部传入初始化并同步。

  • @Once:@Once装饰的变量仅初始化时同步一次,需要与@Param一起使用。

  • @Event:@Event装饰方法类型,作为组件输出,可以通过该方法影响父组件中变量。

  • @Monitor:@Monitor装饰器用于@ComponentV2装饰的自定义组件或@ObservedV2装饰的类中,能够对状态变量进行深度监听。

  • @Provider和@Consumer:用于跨组件层级双向同步。

  • @Computed:计算属性,在被计算的值变化的时候,只会计算一次。主要应用于解决UI多次重用该属性从而重复计算导致的性能问题。

  • !!语法:双向绑定语法糖。

使用限制
由于状态管理V2采用了和状态管理V1不同的实现,因此不能将状态管理框架V2和状态管理V1混合使用,这项限制包括如下场景:

  1. 将V2版本的装饰器装饰的变量传递给V1版本装饰器装饰的变量。
  2. 将V1版本装饰器@Observed、@Track与V2版本装饰器@ObservedV2、@Trace混合使用。
  3. 将V1版本装饰器@State、@Prop、@Link、@ObjectLink、@Provide、@Consume、@StorageLink、@StorageProp、@LocalStorageLink、@LocalStorageProp、@Watch等在@ComponentV2装饰的自定义组件中使用。
  4. 将V2版本装饰器@Local、@Param、@Once、@Event、@Monitor、@Provider、@Consumer在@Component装饰的自定义组件中使用。
  5. 将@Component与@ComponentV2同时使用。
  6. 在@Component装饰的自定义组件中通过@State、@Prop、@Link、@Provide、@Consume、@StorageLink、@StorageProp、@LocalStorageLink、@LocalStorageProp装饰的变量并且该变量的类型为@ObservedV2装饰的类。
  7. 在@ComponentV2装饰的自定义组件中通过@Param、@Local、@Event、@Provider()、@Consumer()装饰的变量并且该变量的类型为@Observed装饰的类。

将V2装饰器与V1装饰器混合使用,会出现未定义行为,表现为冗余刷新、失去深度观测能力、失去自身属性观测能力、失去属性级更新能力等。因此,不能将状态管理V2与状态管理V1在以上提到的场景混合使用。

Logo

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

更多推荐