1 前言

在前文的描述中,我们构建的页面多为静态界面。如果希望构建一个动态的、有交互的界面,就需要引入“状态”的概念。我们本章节来学习状态管理机制

2 概念

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

自定义组件拥有变量,变量必须被装饰器装饰才可以成为状态变量,状态变量的改变会引起UI的渲染刷新。如果不使用状态变量,UI只能在初始化时渲染,后续将不会再刷新。 下图展示了State和View(UI)之间的关系
在这里插入图片描述

  1. View(UI):UI渲染,指将build方法内的UI描述@Builder装饰的方法内的UI描述映射到界面。
  2. State:状态,指驱动UI更新的数据。用户通过触发组件的事件方法,改变状态数据。状态数据的改变,引起UI的重新渲染

3 状态管理


@Component
struct MyComponent {
  // @Prop状态装饰器,状态变量
  @Prop count: number = 0;

  // 常规变量
  private increaseBy: number = 1;

  build() {
    Column() {
      Text("count:"+this.count+" increaseBy:"+this.increaseBy)
        .fontSize(30)
        .fontWeight(FontWeight.Bold)
    }
    .width('100%')
  }
}

@Component
struct Parent {

  // @State状态装饰器,状态变量
  @State count: number = 1;

  build() {
    Column() {
      Button("count++").onClick(()=>{
        console.log("yvan", "count:"+this.count)
        this.count = this.count + 1
      })
      // 从父组件初始化,覆盖本地定义的默认值
      MyComponent({ count: this.count, increaseBy: 2 })
    }
  }
}

在这里插入图片描述

  1. 状态变量被状态装饰器装饰的变量,状态变量值的改变会引起UI的渲染更新。示例中:@State num: number = 1,其中,@State是状态装饰器,num是状态变量。

  2. 常规变量不会引起UI的刷新,示例中increaseBy变量为常规变量

  3. 从父组件初始化:父组件使用命名参数机制,将指定参数传递给子组件。子组件初始化的默认值在有父组件传值的情况下,会被覆盖

  4. 初始化子节点父组件中状态变量可以传递给子组件,初始化子组件对应的状态变量。

4 装饰器

根据状态变量的影响范围,将所有的装饰器可以大致分为管理组件拥有状态的装饰器管理应用拥有状态的装饰器

  1. 管理组件拥有状态的装饰器组件级别的状态管理,可以观察组件内变化,和不同组件层级的变化,但需要唯一观察同一个组件树上,即同一个页面内。

  2. 管理应用拥有状态的装饰器应用级别的状态管理,可以观察不同页面,甚至不同UIAbility的状态变化,是应用内全局的状态管理。

在这里插入图片描述
上图中,Components部分的装饰器为组件级别的状态管理Application部分为应用的状态管理。开发者可以通过@StorageLink/@LocalStorageLink实现应用和组件状态的双向同步,通过@StorageProp/@LocalStorageProp实现应用和组件状态的单向同步

5 组件状态的装饰器

5.1 @State装饰器

@State装饰的变量,或称为状态变量组件内状态,一旦变量拥有了状态属性,就和自定义组件的渲染绑定起来。当状态改变时,UI会发生对应的渲染改变。与声明式范式中的其他被装饰变量一样,是私有的,只能从组件内部访问声明时必须指定其类型和本地初始化。初始化也可选择使用命名参数机制从父组件完成初始化

5.1.1 @State装饰的变量特点

  1. @State装饰的变量与子组件中的@Prop装饰变量之间建立单向数据同步,与@Link@ObjectLink装饰变量之间建立双向数据同步
  2. @State装饰的变量生命周期与其所属自定义组件的生命周期相同。

5.1.2 @State装饰器使用规则

  1. 不支持any,不支持简单类型和复杂类型的联合类型,不允许使用undefinednull
    说明:
  2. 不支持LengthResourceStrResourceColor类型,LengthResourceStrResourceColor为简单类型和复杂类型的联合类型
  3. 类型必须被指定。
  4. 必须本地初始化。

5.1.3 变量的传递/访问规则

在这里插入图片描述

5.1.4 可观察的状态

并不是状态变量的所有更改都会引起UI的刷新,只有可以被框架观察到的修改才会引起UI刷新。该小节去介绍什么样的修改才能被观察到,以及观察到变化后,框架的是怎么引起UI刷新的,即框架的行为表现是什么。

  1. 当装饰的数据类型为booleanstringnumber类型时,可以观察到数值的变化。
  2. 当装饰的数据类型为classObject时,可以观察到自身和其属性赋值的变化,即Object.keys(observedObject)返回的所有属性。简单理解为类一级属性可以观察到变化
  3. 当装饰的对象是array时,可以观察到数组本身的赋值添加删除更新数组的变化。但无法观察array中元素内的属性
  4. 当装饰的对象是Date时,可以观察到Date整体的赋值,同时可通过调用Date的setxxx()方法来更新Date的属性。

当状态变量被改变时,查询依赖该状态变量的组件;执行依赖该状态变量的组件的更新方法,组件更新渲染;和该状态变量不相关的组件或者UI描述不会发生重新渲染,从而实现页面渲染的按需更新。

5.1.5 实例



class W {
  public say: string;
  public p : P = new P("Yvan");
  constructor(say: string) {
    this.say = say;
  }
}

class P {
  public name: string;
  constructor(name: string) {
    this.name = name;
  }
}

@Component
struct MyComponent {
  // 本地初始化
  @State count: number = 0;
  @State w: W = new W('Hello World');

  build() {
    Button(`${this.count}, ${this.w.say}, ${this.w.p.name}`)
      .onClick(() => {
        // 值改变后UI刷新
        this.count += 1;
        this.w.say = 'Hi'
        this.w.p.name = 'Joe'
      })
  }


@Entry
@Component
export struct Index {

  build() {
    Row() {
      // 初始值可传入
      MyComponent({count: 2})
    }
    .height('100%')
  }
}


5.2 @Prop装饰器

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

5.2.1 @Prop装饰的变量特点

  1. 数据源改变会更新@Prop变量
  2. @Prop变量可以本地修改,但不会同步给数据源
  3. @Prop修饰复杂类型时是深拷贝,在拷贝的过程中除了基本类型、Map、Set、Date、Array外,都会丢失类型。
  4. @Prop装饰器不能在@Entry装饰的入口组件中使用

5.2.2 @Prop装饰器使用规则

  1. 对父组件状态变量值的修改,将同步给子组件@Prop装饰的变量,子组件@Prop变量的修改不会同步到父组件的状态变量上,单向同步
  2. @Prop装饰的变量和@State以及其他装饰器同步时双方的类型必须相同
  3. 在组件复用场景,建议@Prop深度嵌套数据不要超过5层,嵌套太多会导致深拷贝占用的空间过大以及GarbageCollection(垃圾回收),引起性能问题,此时更建议使用@ObjectLink。如果子组件的数据不想同步回父组件,建议采用@Reusable中的aboutToReuse,实现父组件向子组件传递数据
  4. @Prop需要被初始化,如果没有进行本地初始化的,则必须通过父组件进行初始化。如果进行了本地初始化,那么是可以不通过父组件进行初始化的。
  5. 允许本地初始化。

5.2.3 变量的传递/访问规则说明

在这里插入图片描述

5.2.4 可观察的状态

基本和5.1.4 @State的观察变化相同,这里列举下不同点:

  1. 对于嵌套场景,如果class是被@Observed装饰的,可以观察到class属性的变化
  2. 除了@State,数据源也可以用@Link或@Prop装饰,对@Prop的同步机制是相同的。
  3. 当父组件的数据源更新时,子组件的@Prop装饰的变量将被来自父组件的数据源重置,所有@Prop装饰的本地的修改将被父组件的更新覆盖。

5.2.5 实例一:简单案例

@Component
struct CountDownComponent {
  @Prop count: number = 0;

  build() {
    Column() {
      Text(`子组件 count:${this.count} `)
      // @Prop装饰的变量不会同步给父组件
      Button(`子组件 count+1`).onClick(() => {
        this.count += 1;
      })
      Button(`子组件 count-1`).onClick(() => {
        this.count -= 1;
      })
    }
  }
}

@Component
struct ParentComponent {
  @State count: number = 1;

  build() {
    Column() {
      Text(`父组件 count:${this.count}`)
      // 父组件的数据源的修改会同步给子组件
      Button(`父组件 count+1`).onClick(() => {
        this.count += 1;
      })
      Button(`父组件 count-1`).onClick(() => {
        this.count -= 1;
      })

      CountDownComponent({ count: this.count })
    }
  }
}


@Entry
@Component
export struct Index {
  build() {
    Row() {
      ParentComponent()
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(VerticalAlign.Center)
  }
}

总结一句话:子组件中@Prop装饰器修饰的变量的变化,不会同步给父组件数据源。父组件的数据源(实例的@State修饰变量)的变化会同步覆盖到子组件@Prop变量。

5.2.6 实例二:嵌套案例

// 以下是嵌套类对象的数据结构。
@Observed
class ClassA {
  public title: string;

  constructor(title: string) {
    this.title = title;
  }
}

@Observed
class ClassB {
  public name: string;
  public a: ClassA;

  constructor(name: string, a: ClassA) {
    this.name = name;
    this.a = a;
  }
}

@Component
struct Parent {
  @State votes: ClassB = new ClassB('Hello', new ClassA('world'))

  build() {
    Column() {
        Button('change ClassB name')
          .onClick(() => {
            // 第一层属性被修改,当前组件、Child组件都能观察到
            this.votes.name = "B name"
          })
        Button('change ClassA title')
          .onClick(() => {
            // 第二层属性被修改,Child不能观察到。
            // @Observed修饰后,Child的值往下传,Child1组件能观察到。
            this.votes.a.title = "A title"
          })
        Child({ vote: this.votes })
    }
  }
}

@Component
struct Child {
  @Prop vote: ClassB = new ClassB('', new ClassA(''));

  build() {
    Column() {
      Text(this.vote.name)
        .onClick(() => {
          // 第一层属性被修改,当前组件都能观察到
          this.vote.name = 'Bye'
        })
      Text(this.vote.a.title)
        .onClick(() => {
          // 当前组件不能观察到
          // @Observed修饰后,Child1组件能观察到。
          this.vote.a.title = "openHarmony"
        })
      Child1({ vote1: this.vote.a })
    }
  }
}

@Component
struct Child1 {
  @Prop vote1: ClassA = new ClassA('');

  build() {
    Column() {
      Text(this.vote1.title)
        .onClick(() => {
          // 只有当前组件能观察
          this.vote1.title = 'Bye Bye'
        })
    }
  }
}

@Entry
@Component
export struct Index {
  build() {
    Row() {
      Parent()
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(VerticalAlign.Center)
  }
}

  1. 在嵌套场景下,每一层都要用@Observed装饰,且每一层都要被@Prop接收,这样才能观察到嵌套。
  2. 在嵌套场景下,也只有第一层属性变化能被观察到,@Observed装饰只能使观察的值往下传,这个地方难以说清楚,看上面Child和Child1的案例表现。

5.3 @Link装饰器

子组件中被@Link装饰的变量与其父组件中对应的数据源建立双向数据绑定,实现组件间父子双向同步。@Link装饰的变量与其父组件中的数据源共享相同的值。但需要注意@Link装饰器不能在@Entry装饰的自定义组件中使用

5.3.1 装饰器使用规则说明

  1. 父组件中@State, @StorageLink和@Link 和子组件@Link可以建立双向数据同步,反之亦然。
  2. 类型必须被指定,且和双向绑定状态变量的类型相同。
  3. 不支持any,不支持简单类型和复杂类型的联合类型,不允许使用undefined和null
  4. 不支持Length、ResourceStr、ResourceColor类型,Length、ResourceStr、ResourceColor为简单类型和复杂类型的联合类型。
  5. 禁止本地初始化。

5.3.2 变量的传递/访问规则说明

在这里插入图片描述

5.3.3 可观察的状态

基本和5.1.4 @State的观察变化相同

5.3.4 实例

class GreenButtonState {
  width: number = 0;

  constructor(width: number) {
    this.width = width;
  }
}

@Component
struct GreenButton {
  @Link greenButtonState: GreenButtonState;

  build() {
    Button('Green Button')
      .width(this.greenButtonState.width)
      .backgroundColor('#64bb5c')
      .onClick(() => {
        // 子组件改变@Link属性,同步到父组件中
        this.greenButtonState.width -= 10;
      })
  }
}

@Component
struct ShufflingContainer {
  @State greenButtonState: GreenButtonState = new GreenButtonState(180);

  build() {
    Column() {
      Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center }) {
        // 从父组件@State向子组件@Link数据同步
        Button('Parent View: Set GreenButton')
          .onClick(() => {
            this.greenButtonState.width += 10;
          })
        // 初始化@Link
        GreenButton({ greenButtonState: $greenButtonState }).margin(12)
      }
    }
  }
}

在子组件中使用@Link装饰状态变量需要保证该变量与数据源类型完全相同,且该数据源需为被诸如@State等装饰器装饰的状态变量

5.4 @Provide装饰器和@Consume装饰器

@Provide和@Consume,应用于与后代组件的双向数据同步,应用于状态数据在多个层级之间传递的场景。不同于上文提到的父子组件之间通过命名参数机制传递,@Provide和@Consume摆脱参数传递机制的束缚,实现跨层级传递

  1. @Provide装饰的变量是在祖先组件中,可以理解为被“提供”给后代的状态变量。
  2. @Consume装饰的变量是在后代组件中,去“消费(绑定)”祖先组件提供的变量。

@Provide@Consume可以通过相同的变量名或者相同的变量别名绑定,建议类型相同@Provide修饰的变量和@Consume修饰的变量是一对多的关系。

@State的规则同样适用于@Provide,差异为@Provide还作为多层后代的同步源

5.4.1 变量的传递/访问规则说明

在这里插入图片描述
在这里插入图片描述

5.4.2 可观察的状态

基本和5.1.4 @State的观察变化相同,我们直接看实例

5.4.3 实例

@Component
struct CompD {
  // @Consume装饰的变量通过相同的属性名绑定其祖先组件CompA内的@Provide装饰的变量
  @Consume reviewVotes: number;

  build() {
    Column() {
      Text(`reviewVotes(${this.reviewVotes})`)
      Button(`reviewVotes(${this.reviewVotes}), give +1`)
        .onClick(() => this.reviewVotes += 1)
    }
    .width('50%')
  }
}

@Component
struct CompC {
  build() {
    Row({ space: 5 }) {
      CompD()
      CompD()
    }
  }
}

@Component
struct CompB {
  build() {
    CompC()
  }
}

@Component
struct CompA {
  // @Provide装饰的变量reviewVotes由入口组件CompA提供其后代组件
  @Provide reviewVotes: number = 0;

  build() {
    Column() {
      Button(`reviewVotes(${this.reviewVotes}), give +1`)
        .onClick(() => this.reviewVotes += 1)
      CompB()
    }
  }
}

@BuilderParam尾随闭包情况下@Provide会报未定义错误,和@BuidlerParam连用的时候要谨慎this的指向。

5.5 @Observed装饰器和@ObjectLink装饰器

上文所述的装饰器仅能观察到第一层的变化,但是在实际应用开发中,应用会根据开发需要,封装自己的数据模型。对于多层嵌套的情况,比如二维数组,或者数组项class,或者class的属性是class,他们的第二层的属性变化是无法观察到的。这就引出了@Observed/@ObjectLink装饰器。
@ObjectLink和@Observed类装饰器用于在涉及嵌套对象或数组的场景中进行双向数据同步

@Observed类装饰器:装饰class。需要放在class的定义前,使用new创建类对象。
@ObjectLink变量装饰器:必须为被@Observed装饰的class实例,必须指定类型。不支持简单类型,可以使用@Prop@ObjectLink的变量只读,但变量的属性可变@ObjectLink装饰的变量不能被赋值,如果要使用赋值操作,请使用@Prop

参考文献:
[1]OpenHarmoney应用开发文档

Logo

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

更多推荐