鸿蒙HarmonyOS开发:状态管理@State、@Prop、@Link、@Provide、@Consume @Watch详解

一、状态管理概述

我们构建的页面多为静态界面。如果希望构建一个动态的、有交互的界面,就需要引入“状态”的概念。

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

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

图1

  • View(UI):UI渲染,指将build方法内的UI描述和@Builder装饰的方法内的UI描述映射到界面。
  • State:状态,指驱动UI更新的数据。用户通过触发组件的事件方法,改变状态数据。状态数据的改变,引起UI的重新渲染。
1、基本概念
  • 状态变量:被状态装饰器装饰的变量,状态变量值的改变会引起UI的渲染更新。示例:@State num: number = 1,其中,@State是状态装饰器,num是状态变量。

  • 常规变量:没有被状态装饰器装饰的变量,通常应用于辅助计算。它的改变永远不会引起UI的刷新。示例:private increaseBy: number = 1。

  • 数据源/同步源:状态变量的原始来源,可以同步给不同的状态数据。通常意义为父组件传给子组件的数据。示例:MyComponent({ count: 1, increaseBy: 2 })。

  • 命名参数机制:父组件通过指定参数传递给子组件的状态变量,为父子传递同步参数的主要手段。示例:CompA: ({ aProp: this.aProp })。

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

@Component
struct MyComponent {
  @State count: number = 0;
  private increaseBy: number = 1;

  build() {
  }
}

@Component
struct Parent {
  build() {
    Column() {
      // 从父组件初始化,覆盖本地定义的默认值
      MyComponent({ count: 1, increaseBy: 2 })
    }
  }
}
  • 初始化子节点:父组件中状态变量可以传递给子组件,初始化子组件对应的状态变量。

  • 本地初始化:在变量声明的时候赋值,作为变量的默认值。示例:@State count: number = 0。

二、@State装饰器:组件内状态

1、概述

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

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

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

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

  • @State装饰的变量与子组件中的@Prop装饰变量之间建立单向数据同步,与@Link、@ObjectLink装饰变量之间建立双向数据同步。
  • @State装饰的变量生命周期与其所属自定义组件的生命周期相同。
2、观察变化
1、当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。
// for simple type
@State count: number = 0;

// value changing can be observed
this.count = 1;
2、当装饰的数据类型为class或者Object时,可以观察到自身的赋值的变化,和其属性赋值的变化,即Object.keys(observedObject)返回的所有属性。
  • 声明ClassA和Model类
class ClassA {
  public value: string;

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

class Model {
  public value: string;
  public name: ClassA;
  constructor(value: string, a: ClassA) {
    this.value = value;
    this.name = a;
  }
}
  • @State装饰的类型是Model
// class类型
@State title: Model = new Model('Hello', new ClassA('World'));
  • 对@State装饰变量的赋值。
// class类型赋值
this.title = new Model('Hi', new ClassA('ArkUI'));
  • 对@State装饰变量的属性赋值。
// class属性的赋值
this.title.value = 'Hi';
  • 嵌套属性的赋值观察不到。
// 嵌套的属性赋值观察不到
this.title.name.value = 'ArkUI';
3、当装饰的对象是array时,可以观察到数组本身的赋值和添加、删除、更新数组的变化。
  • 声明Model类。
class Model {
  public value: number;
  constructor(value: number) {
    this.value = value;
  }
}
  • @State装饰的对象为Model类型数组时。
@State title: Model[] = [new Model(11), new Model(1)];
  • 数组自身的赋值可以观察到。
this.title = [new Model(2)];
  • 数组项的赋值可以观察到。
this.title[0] = new Model(2);
  • 删除数组项可以观察到。
this.title.pop();
  • 新增数组项可以观察到。
this.title.push(new Model(12));
  • 数组项中属性的赋值观察不到。
this.title[0].value = 6;
3、使用演示
@Entry
@Component
struct MyComponent {
  @State count: number = 0;

  build() {
    Button(`click times: ${this.count}`)
      .onClick(() => {
        this.count += 1;
      })
  }
}

三、@Prop装饰器:父子单向同步

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

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

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

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

1、使用演示
@Component
struct CountDownComponent {
  @Prop count: number;
  build() {
    Column() {
      Text(`这是子组件的值:${this.count},改变不会影响到父组件。`)
      Row(){
        // @Prop装饰的变量不会同步给父组件
        Button(`-1`).onClick(() => {
          this.count -= 1
        })
        Button(`+1`).onClick(() => {
          this.count += 1
        })
      }
      .justifyContent(FlexAlign.SpaceEvenly)
      .width("100%")
    }
  }
}

@Entry
@Component
struct ParentComponent {
  @State countDownStartValue: number = 10;

  build() {
    Column() {
      Text(`这是父组件的值:${this.countDownStartValue},改变会传给子组件。`)
      Row(){
        // 父组件的数据源的修改会同步给子组件
        Button(`+1`).onClick(() => {
          this.countDownStartValue += 1;
        })
        Button(`-1`).onClick(() => {
          this.countDownStartValue -= 1;
        })
      }
      .justifyContent(FlexAlign.SpaceEvenly)
      .width("100%")

      // 子组件
      CountDownComponent({ count: this.countDownStartValue })
    }
  }
}

四、@Link装饰器:父子双向同步

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

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

1、使用演示
@Component
struct CountDownComponent {
  @Link count: number;
  build() {
    Column() {
      Text(`这是子组件的值:${this.count},改变会影响到父组件。`)
      Row(){
        // @Prop装饰的变量会同步给父组件
        Button(`+1`).onClick(() => {
          this.count += 1
        })
        Button(`-1`).onClick(() => {
          this.count -= 1
        })
      }
      .justifyContent(FlexAlign.SpaceEvenly)
      .width("100%")
    }
  }
}

@Entry
@Component
struct ParentComponent {
  @State countDownStartValue: number = 10;

  build() {
    Column() {
      Text(`这是父组件的值:${this.countDownStartValue},改变会传给子组件。`)
      Row(){
        // 父组件的数据源的修改会同步给子组件
        Button(`+1`).onClick(() => {
          this.countDownStartValue += 1;
        })
        Button(`-1`).onClick(() => {
          this.countDownStartValue -= 1;
        })
      }
      .justifyContent(FlexAlign.SpaceEvenly)
      .width("100%")

      // 子组件
      CountDownComponent({ count: $countDownStartValue })
      
    }
  }
}

五、@Provide装饰器和@Consume装饰器:与后代组件双向同步

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

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

@Provide/@Consume装饰的状态变量有以下特性:

  • @Provide装饰的状态变量自动对其所有后代组件可用,即该变量被“provide”给他的后代组件。由此可见,@Provide的方便之处在于,开发者不需要多次在组件之间传递变量。
  • 后代通过使用@Consume去获取@Provide提供的变量,建立在@Provide和@Consume之间的双向数据同步,与@State/@Link不同的是,前者可以在多层级的父子组件之间传递。
  • @Provide和@Consume可以通过相同的变量名或者相同的变量别名绑定,变量类型必须相同。
// 通过相同的变量名绑定
@Provide a: number = 0;
@Consume a: number;

// 通过相同的变量别名绑定
@Provide('a') b: number = 0;
@Consume('a') c: number;
1、使用演示

// 孙组件
@Component
struct CountDownComponentB {
  @Consume count: number;
  build() {
    Column() {
      Text(`这是孙组件的值:${this.count},改变会影响到父组件。`)
      Row(){
        Button(`+1`).onClick(() => {
          this.count += 1
        })
        Button(`-1`).onClick(() => {
          this.count -= 1
        })
      }
      .justifyContent(FlexAlign.SpaceEvenly)
      .width("100%")
    }
  }
}

// 子组件
@Component
struct CountDownComponentA {
  build() {
    Column() {
      // 孙组件
      CountDownComponentB()
    }
  }
}


// 父组件
@Entry
@Component
struct ParentComponent {
  @Provide count: number = 10;

  build() {
    Column() {
      Text(`这是父组件的值:${this.count},改变会传给孙组件。`)
      Row(){
        Button(`+1`).onClick(() => {
          this.count += 1;
        })
        Button(`-1`).onClick(() => {
          this.count -= 1;
        })
      }
      .justifyContent(FlexAlign.SpaceEvenly)
      .width("100%")

      // 子组件
      CountDownComponentA()

    }
  }
}

六、@Watch装饰器:状态变量更改通知

@Watch应用于对状态变量的监听。如果开发者需要关注某个状态变量的值是否改变,可以使用@Watch为状态变量设置回调函数。

当状态变量变化时,@Watch的回调方法将被调用。@Watch在ArkUI框架内部判断数值有无更新使用的是严格相等(===),遵循严格相等规范。当在严格相等为false的情况下,就会触发@Watch的回调。

@Watch可用于购物车计算总价,或者实现计算器功能等。

(changedPropertyName? : string) => void

  • 建议开发者避免无限循环。循环可能是因为在@Watch的回调方法里直接或者间接地修改了同一个状态变量引起的。为了避免循环的产生,建议不要在@Watch的回调方法里修改当前装饰的状态变量;
  • 开发者应关注性能,属性值更新函数会延迟组件的重新渲染(具体请见上面的行为表现),因此,回调函数应仅执行快速运算;
  • 不建议在@Watch函数中调用async await,因为@Watch设计的用途是为了快速的计算,异步行为可能会导致重新渲染速度的性能问题。
1、使用演示
/*
 * @Watch 修饰  状态数据
 *  函数中,不要修改被监视的状态变量。 我们要操作的是其他的业务逻辑
 * */
@Entry
@Component
struct WatchDct {
  @State @Watch('change') count: number = 1
  @State @Watch('change') pow: number = 2
  @State res: number = 1

  change() {
    this.res = Math.pow(this.count, this.pow)
  }

  build() {
    Row() {
      Column() {
        Text('基数:' + this.count)
          .fontSize(50)
          .onClick(() => {
            this.count++
          })

        Divider()
        Text(`次幂:${this.pow}`)
          .fontSize(50)
          .onClick(() => {
            this.pow++
          })

        Divider()
        Text("结果:" + this.res)
          .fontSize(50)
      }
      .width('100%')
    }
    .height('100%')
  }
}
  • 21
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

邹荣乐

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

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

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

打赏作者

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

抵扣说明:

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

余额充值