Bootstrap

鸿蒙进阶篇-状态管理之@Prop&@Link

大家好啊,这里是鸿蒙开天组,今天我们来学习状态管理。

开始组件化开发之后,如何管理组件的状态会变得尤为重要,咱们接下来系统的学习一下这部分的内容

状态管理机制

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

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

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

基本概念

接下来咱们同步一下一些关键词的称呼

  • 状态变量:被状态装饰器装饰的变量,状态变量值的改变会引起UI的渲染更新。示例:@State num: number = 1,其中,@State是状态装饰器,num是状态变量。
  • 常规变量:没有被状态装饰器装饰的变量,通常应用于辅助计算。它的改变永远不会引起UI的刷新。以下示例中increaseBy变量为常规变量。
  • 数据源/同步源:状态变量的原始来源,可以同步给不同的状态数据。通常意义为父组件传给子组件的数据。以下示例中数据源为count: 1。
  • 命名参数机制:父组件通过指定参数传递给子组件的状态变量,为父子传递同步参数的主要手段。示例:CompA: ({ aProp: this.aProp })。
  • 从父组件初始化:父组件使用命名参数机制,将指定参数传递给子组件。子组件初始化的默认值在有父组件传值的情况下,会被覆盖。示例:
@Entry
@Component
struct Parent {
  build() {
    Column() {
      // 从父组件初始化,覆盖本地定义的默认值
      ChildComponent({ count: 1,})
    }
  }
}

@Component
struct ChildComponent {
  // 状态变量,更改会触发 UI 刷新
  @State count: number = 0;
  build() {
      Text(this.count.toString())
  }
}
  • 初始化子组件:父组件中状态变量可以传递给子组件,初始化子组件对应的状态变量。示例同上。
  • 本地初始化:在变量声明的时候赋值,作为变量的默认值。示例:@State count: number = 0。

装饰器总览

ArkUI提供了多种【装饰器】,通过使用这些装饰器,状态变量不仅可以观察在组件内的改变,还可以在不同组件层级间传递,比如父子组件、跨组件层级,也可以观察全局范围内的变化。根据状态变量的影响范围,将所有的装饰器可以大致分为:

  • 管理组件拥有状态的装饰器:组件级别的状态管理,可以观察组件内变化,和不同组件层级的变化,但需要唯一观察同一个组件树上,即同一个页面内。
  • 管理应用拥有状态的装饰器:应用级别的状态管理,可以观察不同页面,甚至不同UIAbility的状态变化,是应用内全局的状态管理。

下面这张图就是完整的装饰器说明图,咱们后续的学习就围绕着这张图来展开

  1. 管理组件状态:小框中(目前专注这个即可)
  2. 管理应用状态:大框中

@State 自己的状态

@State 装饰器咱们已经学习过了,所以就不从头讲解,而是说2 个使用的注意点

观察变化注意点:

并不是状态变量的所有更改都会引起UI的刷新,只有可以【被框架观察到】的修改才会引起UI刷新。

  • 当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。
  • 当装饰的数据类型为class或者Object时,可以观察到自身的赋值的变化,和其属性赋值的变化,即Object.keys(observedObject)返回的所有属性。例子如下。声明ClassA和Model类。
// 基本数据类型
@State count: number = 0;
// 可以观察到数值变化
this.count = 1;

//Object.keys 测试

interface Chicken {
  name: string
  age: number
  color: string
}

const c: Chicken = {
  name: '1',
  age: 2,
  color: '黄绿色'
}
// 获取对象的属性名,返回字符串数组
console.log('', Object.keys(c))// name,age,color

//复杂数据类型且嵌套


interface Dog {
  name: string
}

interface Person {
  name: string
  dog: Dog
}

@Component
export struct HelloComponent {
  // 状态变量
  @State message: string = 'Hello, World!';
  
  @State person: Person = {
    name: 'jack',
    // 嵌套属性
    dog: {
      name: '柯基'
    }
  }

  sayHi() {
    console.log('你好呀')
  }

  build() {
    Column() {
      Text(this.message)
      Button('修改 message')
        .onClick(() => {
          this.message = 'Hello,ArkTS'
        })
      Text(JSON.stringify(this.person))
      Button('修改title外层属性')
        .onClick(() => {
          this.person.name = '666'
        })
      Button('修改title嵌套属性')
        .onClick(() => {
          // 修改嵌套属性,无法被监听,UI 不更新
          this.person.dog.name = '内部的 666'

          // 修改第一层属性,可以被监听,UI 更新
          // this.person.dog = {
          //   name: '阿拉斯加'
          // }
        })


    }
  }
}

@Prop 父子单向

@Prop 装饰的变量可以和父组件建立单向的同步关系。@Prop 装饰的变量是可变的,但是变化不会同步回其父组件。(不要直接修改 Prop 的值)

@Component
struct SonCom {
  // 默认值可以省略,不设置为 undefined
  @Prop xxx:类型='可选默认值' 
  build() {
  }
}

@Entry
@Component
  // FatherCom 父组件
struct FatherCom {
  build() {
    Column() {
      // 子组件
      SonCom({xxx:'具体的值'})
    }
  }
}

@Link双向同步

使用步骤:

  1. 将父组件的状态属性传递给子组件
  2. 子组件通过@Link修饰即可
  3. 分别测试基础,和复杂类型

基础模板

@Entry
@Component
  // 父组件
struct KnowledgePage {
  @State count: number = 0

  build() {
    Column() {
      Text('父组件')
        .fontSize(30)
      Text(this.count.toString())
      Button('修改数据')
        .onClick(() => {
        })


      SonComponent()
    }
    .padding(10)
    .height('100%')
    .backgroundColor('#ccc')
    .width('100%')
    .alignItems(HorizontalAlign.Center)
    .padding({ top: 100 })
  }
}


@Component
  // 子组件
struct SonComponent {
  // 编写 UI
  build() {
    Column({ space: 20 }) {
      Text('我是子组件')
        .fontSize(20)


      Column() {
        Button('修改count')
          .onClick(() => {
          })

      }

    }
    .backgroundColor('#a6c398')
    .alignItems(HorizontalAlign.Center)
    .width('80%')
    .margin({ top: 100 })
    .padding(10)
    .borderRadius(10)

  }
}

参考代码

interface Person {
  name: string
  age: number
}


@Entry
@Component
  // 父组件
struct Page03_Link {
  @State count: number = 0
  @State p: Person = {
    name: 'jack',
    age: 18,
  }

  build() {
    Column() {
      Text('父组件')
        .fontSize(30)
      Text(this.count.toString())
      Text(JSON.stringify(this.p))
      Button('修改数据')
        .onClick(() => {
          this.count--
        })


      SonComponent({
        count: this.count,
        p: this.p
      })
    }
    .padding(10)
    .height('100%')
    .backgroundColor('#ccc')
    .width('100%')
    .alignItems(HorizontalAlign.Center)
    .padding({ top: 100 })
  }
}


@Component
  // 子组件
struct SonComponent {
  // 通过 @Link 来接收
  @Link count: number
  @Link p: Person

  // 编写 UI
  build() {
    Column({ space: 20 }) {
      Text('我是子组件')
        .fontSize(20)
      Text(this.count.toString())
      Text(JSON.stringify(this.p))

      Column() {
        Button('修改count')
          .onClick(() => {
            this.count++
          })
        Button('修改Person')
          .onClick(() => {
            this.p.age++
          })

      }

    }
    .backgroundColor('#a6c398')
    .alignItems(HorizontalAlign.Center)
    .width('80%')
    .margin({ top: 100 })
    .padding(10)
    .borderRadius(10)

  }
}

好啦,今天的内容就到这里,感谢大家的观看,我们下次再见!

;