ArkTS状态管理V1

ArkTS状态管理V1

1.状态管理概述

在我们开发的用户界面(UI)中,所有的内容可以看作两部分,一部分是组件本身(它描述了组件的显示样式、位置、布局方式等),还有一部分就是组件内显示的数据。
ArkUI是采用数据驱动UI更新的模式进行状态管理的。如果希望在程序运行时,组件内的数据发生改变同步更新UI界面,这就需要用到ArkUI提供的状态管理机制。
实现状态管理需要两个前提
● 需要有状态变量:ArkUI提供了若干个状态装饰器,被状态装饰器修饰的变量才称为状态变量。
● 状态变量被UI组件引用:当状态变量发生改变时,引用该状态变量的UI组件会被重新渲染(build()函数重新执行)。

在这里插入图片描述

注意:想要数据成为状态变量,必须被状态修饰符修饰(如@State、@Prop、@Link、@Provide、@Consume等),如果变量没有被状态装饰器,是无法引起UI更新的。

2. @State 组件内状态

@State装饰的变量,或称为状态变量,一旦变量拥有了状态属性,就可以触发其直接绑定UI组件的刷新。但是,并不是状态变量的所有更改都会引起UI的刷新,只有可以被框架观察到的修改才会引起UI刷新。

● 当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化
● 当装饰的数据类型为class或者Object时,可以观察到自身的赋值的变化,和属性赋值的变化,即Object.keys(observedObject)返回的所有属性。

2.1 装饰简单类型变量

如果被观察的数据是简单类型:boolean、string、number类型时,都可以引起UI更新。下面以string类型为例。
如下图所示:点击按钮时,改变文本显示内容
在这里插入图片描述

@Entry
@Component
struct Index {
  @State message: string = 'HelloWorld'

  build() {
    Column() {
      Text(this.message).fontSize(24)

      Button('改变状态变量').margin({ top: 20 })
        .onClick(() => {
          this.message = this.message === 'HelloWorld' ? 'HarmonyOS' : 'HelloWorld'
        })
    }.width('100%')
    .alignItems(HorizontalAlign.Center)
  }
}

2.2 装饰对象类型变量

在下面的示例中,使用@State装饰类型为Person的变量,点击Button改变Person对象本身或者属性值,视图会随之刷新。
在这里插入图片描述

import { JSON } from '@kit.ArkTS'

class Person {
  name: string
  age: number
  car: Car

  constructor(name: string, age: number, car: Car) {
    this.name = name
    this.age = age
    this.car = car
  }
}

class Car {
  brand: string

  constructor(brand: string) {
    this.brand = brand
  }
}
@Entry
@Component
struct Index {
  @State person: Person = new Person('余承东', 54, new Car('问界M9'))

  build() {
    Column() {
      Text(JSON.stringify(this.person)).fontSize(24)
      Button('更改对象属性').onClick(()=>{
        this.person.name = '雷军'
        this.person.age = 55
        this.person.car.brand = '小米SU7' 
      })
    }.width('100%').height('100%')
  }
}

注意:如果单独更改对象的嵌套属性,是无法更新UI的

2.3 装饰Map类型变量

在下面的示例中,使用@State装饰类型为Map<number, string>的变量,点击Button改变Map的值,视图会随之刷新。
在这里插入图片描述

@Entry
@Component
struct Index {
  @State map: Map<string, number>
        = new Map([['马云', 50], ['任正非', 79], ['雷军', 55]])

  build() {
    Column({space:10}) {
      ForEach(Array.from(this.map.entries()), (item: [string, number]) => {
        Text(item[0] + ":" + item[1])
      })
      Button('更改整个Map').onClick(() => {
        this.map = new Map([['马化腾', 52], ['刘强东', 50], ['李彦宏', 54]])
      })

      Button('往Map中添加键值对').onClick(() => {
        this.map.set('余承东',55)
      })

      Button('修改Map中的键值对').onClick(() => {
        this.map.set('余承东',60)
      })

      Button('删除Map中的键值对').onClick(() => {
        this.map.delete('余承东')
      })
    }.width('100%').height('100%') 
  }
}

2.4 装饰Set类型变量

在下面的示例中,使用@State装饰类型为Set的变量,点击Button改变Set的值,视图会随之刷新。
在这里插入图片描述

@Entry
@Component
struct Index {
  @State set: Set<string> = new Set(['小明', '小强', '小刚'])

  build() {
    Column({ space: 10 }) {
      ForEach(Array.from(this.set), (item:string) => {
        Text(item).fontSize(24)
      })
      Button('更改整个Set').onClick(() => {
        this.set = new Set(['xiaoming','xiaoqiang','xiaogang'])
      })

      Button('往Set中添加元素').onClick(() => {
        this.set.add('小智')
      })

      Button('删除Set中的元素').onClick(() => {
        this.set.delete('小智')
      })

      Button('清空Set中的元素').onClick(() => {
        this.set.clear()
      })
    }.width('100%').height('100%')
  }
}

3. @Prop 父子单向同步

@Prop 装饰的变量可以和父组件建立单向的同步关系。@Prop装饰的变量是可变的,但是变化不会同步回其父组件。
如下图所示,点击更改父组件状态时,子组件能同步更新;但是,此时更改子组件状态变量时,不能把状态传递给父组件。
在这里插入图片描述

3.1 父组件更新子组件UI

把父组件的状态变量传递给子组件,当父组件状态变量发生改变时,子组件同步更新UI。步骤如下

  1. 使用@State装饰父组件变量,使其成为状态变量
  2. 使用@Prop装饰子组件变量,使其和父组件变量形成单向同步关系(父->子)
  3. 将父组件中的状态变量的值,通过子组件构造器传递给子组件
  • 第1步:使用@State装饰父组件变量
@Component
struct FComponent {
  //1.使用@State修饰父组件变量
  @State fHouse: string = '农村小平房'

  build() {
    Column() {
      Text(`父组件-${this.fHouse}`)
      Button('换房子').onClick(() => {
        this.fHouse = '商品房'
      })
    }.width(250).height(250).backgroundColor(Color.Orange)
  }
}
  • 第2步:使用@Prop修饰子组件变量
@Component
struct SComponent {
  //2.使用@Prop修饰子组件变量
  @Prop house: string = ''
  build() {
    Column() {
      Text(`子组件-${this.house}`).fontSize(20)
      Button('换房子').onClick(() => {
        this.house = '大别墅'
      })
    }
    .width(150).height(150).backgroundColor(Color.Pink)
  }
}
  • 第3步:将父组件中的状态变量传递给子组件
@Component
struct FComponent {
  @State fHouse: string = '农村小平房'

  build() {
    Column() {
      Text(`父组件-${this.fHouse}`)
      Button('换房子').onClick(() => {
        this.fHouse = '商品房'
      })

      //3.将父组件的状态变量,传递给子组件
      SComponent({
        house: this.fHouse
      })
    }.width(250).height(250).backgroundColor(Color.Orange)
  }
}

3.2 子组件更新父组件UI

在这里插入图片描述
子组件是不能直接更改父组件状态变量,而是采用曲线救国的策略,具体步骤如下

  1. 我们可以在父组件中定义一个修改父组件状态的函数
  2. 然后将函数传递给子组件
  3. 在子组件中回调父组件传递过来的函数,修改父组件的状态

如图所示:实际上也是父组件更改自己的状态
在这里插入图片描述
完整代码如下

@Entry
@Component
struct Index {
  build() {
    Column() {
      FComponent()
    }.width('100%').height('100%')
  }
}


@Component
struct FComponent {
  @State fHouse: string = '农村小平房'

  build() {
    Column() {
      Text(`父组件-${this.fHouse}`)
      Button('换房子').onClick(() => {
        this.fHouse = '商品房'
      })

      SComponent({
        house: this.fHouse,
        changeHouse: (house: string) => {
          this.fHouse = house
        }
      })
    }.width(250).height(250).backgroundColor(Color.Orange)
  }
}

@Component
struct SComponent {
  @Prop house: string = ''
  changeHouse = (house: string) => {
  }

  build() {
    Column() {
      Text(`子组件-${this.house}`).fontSize(20)
      Button('换房子').onClick(() => {
        this.changeHouse('大别墅')
      })
    }
    .width(150).height(150).backgroundColor(Color.Pink)
  }
}

子组件中被@Link装饰的变量与其父组件中对应的数据源建立双向数据绑定。也就是说,父组件可以把状态改变传给子组件;子组件也可以把状态改变传给父组件。如下图所示
在这里插入图片描述
实现父子双向数据绑定的具体步骤如下

  1. 先使用@State装饰父组件状态变量
  2. 使用@Link装饰子组件状态变量
  3. 将父组件的状态变量传递给子组件

4.1 简单类型数据同步

  • 第1步:先试用@State装饰父组件状态变量
@Entry
@Component
struct Index {
  build() {
    Column() {
      FuComponent()
    }.width('100%').height('100%')
  }
}

@Component
struct FuComponent {
  @State info: string = '床前明月光'

  build() {
    Column({ space: 20 }) {
      Text('父组件').fontSize(24).fontWeight(FontWeight.Bold)
      Text(this.info)
      Button('修改数据').onClick(() => {
        this.info = '疑是地上霜'
      })

    }.padding(10).backgroundColor(Color.Pink)
  }
}
  • 第2步:使用@Link装饰子组件状态变量
@Component
struct SonCompoent {
  @Link info: string

  build() {
    Column({ space: 10 }) {
      Text('子组件').fontSize(24).fontWeight(FontWeight.Bold)
      Text(this.info)
      Button('修改数据').onClick(() => {
        this.info = '举头望明月,低头思故乡'
      })
    }.width('100%').height(200).backgroundColor('#A4BE97').border({ radius: 10 })
  }
}
  • 第3步:将父组件状态变量传递给子组件
@Component
  struct FuComponent {
    @State info: string = '床前明月光'

    build() {
      Column({ space: 20 }) {
        Text('父组件').fontSize(24).fontWeight(FontWeight.Bold)
        Text(this.info)
        Button('修改数据').onClick(() => {
          this.info = '疑是地上霜'
        })

        SonCompoent({ info: this.info })
          .margin({ top: 20 })
      }.padding(10).backgroundColor(Color.Pink)
    }
  }

4.2 复杂类型数据同步

@Link除了装饰简单类型变量之外,还可以装饰复杂类型数据,如对象、Map、Set等,都可以使用@Link进行双向数据同步。
在这里插入图片描述
实现上述案例,代码如下

import { JSON } from '@kit.ArkTS'
class Person {
  name: string
  age: number

  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }
}

@Entry
@Component
struct Index {
  build() {
    Column() {
      FuComponent()
    }.width('100%').height('100%')
  }
}

@Component
struct FuComponent {
  @State info: Person = new Person('雷军', 55)

  build() {
    Column({ space: 20 }) {
      Text('父组件').fontSize(24).fontWeight(FontWeight.Bold)
      Text(JSON.stringify(this.info))
      Button('修改数据').onClick(() => {
        this.info.name = '雷布斯'
      })

      SonCompoent({ info: this.info }).margin({ top: 20 })
    }.padding(10).backgroundColor(Color.Pink)
  }
}

@Component
struct SonCompoent {
  @Link info: Person

  build() {
    Column({ space: 10 }) {
      Text('子组件').fontSize(24).fontWeight(FontWeight.Bold)
      Text(JSON.stringify(this.info))
      Button('修改数据').onClick(() => {
        this.info.name = '猴王'
        this.info.age = 10
      })
    }.width('100%').height(200).backgroundColor('#A4BE97').border({ radius: 10 })
  }
}

5.@Provide/@Consum 跨层级双向同步

如果组件的层级比较深,如果使用@Link装饰器就需要一层一层传递,比较麻烦;而@Provide/@Consum 可以实现跨层级数据的双向绑定
如下图所示,有三个层级的组件,顶层组件数据的变化可以直接跨层级传给内层组件,内层组件的数据也可以跨层级直接传递给外层。

在这里插入图片描述
代码实现步骤如下

  1. 先试用@Provide装饰父组件状态变量
  2. 使用@Consum装饰子组及后代组件状态变量

5.1 顶层组件

//这是顶层组件
@Entry
@Component
struct Index {
  @Provide count: number = 0

  build() {
    Column() {
      Text(`我是顶层组件`)
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 50 })
      
      Text(`${this.count}`).fontSize(30).fontColor(Color.Red)
        .onClick(() => {
          this.count++
        })
      SecondComponent().margin({ top: 10 })
    }.width('100%').height('100%').backgroundColor('#C6C6C6').padding({ left: 12, right: 12 })
  }
}

5.2 中层组件

/这是二级组件
@Component
struct SecondComponent {
  @Consume count: number

  build() {
    Column() {
      Text('我是二级组件').fontSize(20).fontWeight(FontWeight.Bold)
      Text(`${this.count}`).fontSize(30).fontColor(Color.Orange).onClick(() => {
        this.count += 2
      })
      ThridComponent().margin({ right: 15, left: 15 })
    }
    .width('100%')
    .height(200)
    .backgroundColor('#A4BE97')
    .border({ radius: 15 })
    .padding({ top: 12, bottom: 12 })
    .justifyContent(FlexAlign.SpaceBetween)
  }
}

5.3 内层组件

/这是三级组件
@Component
struct ThridComponent {
  @Consume count: number

  build() {
    Column() {
      Text('我是内层组件')
      Text(`${this.count}`).fontSize(30).fontColor(Color.White).onClick(() => {
        this.count--
      })
    }
    .width('100%')
    .height(50)
    .backgroundColor('#AF88D7')
    .justifyContent(FlexAlign.Center)
    .border({ radius: 15 })
  }
}

上文所述的装饰器(包括@State、@Prop、@Link、@Provide和@Consume装饰器)仅能观察到对象第一层的属性变化,但是在实际应用开发中,应用会根据开发需要,封装自己的数据模型。对于多层嵌套的情况,比如二维数组,或者数组项class,或者class的属性是class,他们的第二层的属性变化是无法观察到的。这就引出了@Observed/@ObjectLink装饰器。

6.1 观察嵌套类对象属性变化存在的问题

● 假设有如下Person和Car类;
● 如下图所示,观察当修改person.car.brand属性值时,是否会触发UI更新;
在这里插入图片描述
代码如下

class Person {
  name: string
  age: number
  car: Car

  constructor(name: string, age: number, car: Car) {
    this.name = name
    this.age = age
    this.car = car
  }
}

class Car {
  brand: string

  constructor(brand: string) {
    this.brand = brand
  }
}

@Entry
@Component
struct ObservedPage {
  @State person: Person = new Person("张三", 20, new Car("保时捷"))

  build() {
    Column({space:10}) {
      Text(`${this.person.name}`)
      Text(`${this.person.age}`)
      Text(`${this.person.car.brand}`)

      Button("更新name").onClick(() => {
        this.person.name = "李四"
      })

      Button("更新age").onClick(() => {
        this.person.age = 33
      })

      Button("更新car.brand").onClick(() => {
        this.person.car.brand = "拖拉机"   //无法观察到UI的变化
      })
    }
    .height('100%')
    .width('100%')
  }
}

⚠️ 结论:当修改嵌套对象的属性时,无法被观察到,UI不会刷新;

6.2 观察嵌套类对象属性变化

为了让嵌套类对象的属性能够被观察到,需要进行如下修改
❶ 给Person和Car类加上@Observed装饰

@Observed
class Person {
  name: string
  age: number
  car: Car

  constructor(name: string, age: number, car: Car) {
    this.name = name
    this.age = age
    this.car = car
  }
} 

@Observed
class Car {
  brand: string
  constructor(brand: string) {
    this.brand = brand
  }
}

❷ 将引用car.brand属性的UI抽取出来,成为自定义组件Child,并在页面中引用Chid组件,并传递this.person.car对象

@Component
struct Child {
  @ObjectLink car: Car

  build() {
    Column({ space: 10 }) {
      Text(`person.car.brand: ${this.car.brand}`)
    }
  }
}

@Entry
@Component
struct ObservedPage {
  @State person: Person = new Person("张三", 20, new Car("保时捷"))

  build() {
    Column({space:10}) {
      Text(`person.name: ${this.person.name}`)
      Text(`person.age: ${this.person.age}`)
      Child({
        car: this.person.car
      })

      Button("更新name").onClick(() => {
        this.person.name = "李四"
      })

      Button("更新age").onClick(() => {
        this.person.age = 33
      })

      Button("更新car.brand").onClick(() => {
        this.person.car.brand = "拖拉机"
      })

    }
    .height('100%')
    .width('100%')
  }
}

6.3 观察对象数组的属性变化

如下图所示,在一个对象数组中存储了多个Person对象,并以列表的形式展示;点击“修改”按钮时,修改数组对象元素的属性。
在这里插入图片描述

@Observed
class Person {
  name: string
  age: number
  car: Car

  constructor(name: string, age: number, car: Car) {
    this.name = name
    this.age = age
    this.car = car
  }
}

@Observed
class Car {
  brand: string

  constructor(brand: string) {
    this.brand = brand
  }
} 

@Component
struct Child {
  @ObjectLink car: Car

  build() {
    Column({ space: 10 }) {
      Text(`${this.car.brand}`)
    }
  }
}

@Component
struct MyListItem {
  @ObjectLink person: Person

  build() {
    Row({ space: 10 }) {
      Text(`${this.person.name}`)
      Text(`${this.person.age}`)
      Child({
        car: this.person.car
      })
    }.width("100%").justifyContent(FlexAlign.SpaceAround)
  }
}


@Entry
@Component
struct ObservedPage {
  @State persons: Person[] = [
    new Person("张三", 20, new Car("保时捷")),
    new Person("李四", 20, new Car("法拉利")),
    new Person("王五", 20, new Car("阿斯顿马丁")),
    new Person("找刘", 20, new Car("布加迪")),
  ]

  build() {
    Column({ space: 10 }) {
      ForEach(this.persons, (person: Person) => {
        MyListItem({ person: person })
      })

      Button("修改第一条数据的汽车品牌").onClick(() => {
        this.persons[0].car.brand = "卡宴"
      })
    }
    .height('100%')
    .width('100%')
  }
}

7.@Watch 监视器

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

7.1 监听@State变量

@Entry
@Component
struct Index {
  @State @Watch("countChange") count: number = 0 

  countChange() {
    console.log("检测到count发生改变了:count=" + this.count)
  }

  build() {
    Column() {
      Text(`${this.count}`)

      Button('更新')
        .onClick(() => {
          this.count++
        })
    }
    .width('100%')
    .height('100%')
  }
}

7.2 监听@Prop变量

@Watch可以监听@Prop状态变量。当父组件的count发生变化时,会将变化传递给子组件,所以在子组件中也能监听到状态的变化。

注意:由于@Prop是父子单向数据传递的,所以子组件中count的变化,在父组件中不能被观察到。

在这里插入图片描述

//定义子组件
@Component
struct MyComponent {
  @Prop @Watch("onCountChange") count: number

  onCountChange(){
    console.log("观察到子组件count变化"+this.count)
  }

  build() {
    Column() {
      Text(`${this.count}`)
    }
  }
}

//入口组件
@Entry
@Component
struct Index {
  @State count: number = 0

  build() {
    Column() {
      MyComponent({
        count:this.count
      })
      Button("父组件状态更新")
        .onClick(()=>{
          this.count++
        })
    }
    .width('100%')
    .height('100%')
  }
}

7.3 监听@Link变量

@Watch可以监听@Link状态变量。由于@State和@Link是双向同步的,所以当父组件的count发生变化时,会将变化传递给子组件,所以在子组件中可以监听到count状态的变化;反过来子组件中count状态发生改变时,也会将变化传递给父组件,在父组件中也能监听到count的变化。
在这里插入图片描述

@Component
struct MyComponent {
  @Link @Watch("onCountChange") count: number

  onCountChange() {
    console.log("观察到子组件count变化" + this.count)
  }

  build() {
    Column() {
      Text(`${this.count}`)
      Button('子组件更新')
        .onClick(()=>{
          this.count++
        })
    }.border({
      width: 1,
      color: Color.Black,
      style: BorderStyle.Solid
    })
    .width(200)
    .height(100)
  }
}

@Entry
@Component
struct Index {
  @State @Watch("onCountChange") count: number = 0

  onCountChange() {
    console.log("观察到父组件count变化" + this.count)
  }

  build() {
    Column() {
      MyComponent({
        count: this.count
      })
      Button("父组件状态更新")
        .onClick(() => {
          this.count++
        })
    }
    .border({
      width: 1,
      color: Color.Black,
      style: BorderStyle.Solid
    })
    .width(300)
    .height(200)
    .justifyContent(FlexAlign.Center)
  }
}

8.@Track 装饰器

@Track应用于class对象的属性级更新。@Track装饰的属性变化时,只会触发该属性关联的UI更新。使用@Track装饰器可以避免冗余刷新。

8.1 冗余刷新问题

如下图所示:当更新age时,虽然只更新了age的值,但是name和age都会重新渲染,这就叫做冗余刷新。
在这里插入图片描述
为了验证name和age的UI重新渲染,我们将Text的样式封装成了方法,如果该方法重复执行,则表示UI重新渲染了。

class Student {
  name: string
  age: number

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

@Entry
@Component
struct Index {
  @State student: Student = new Student("张三", 30)

  build() {
    Row() {
      Column() {
        Text(this.student.name).fontSize(this.getSize(1))
        Text(`${this.student.age}`).fontSize(this.getSize(2)) 
        Button("更新age").onClick(() => {
          this.student.age++
        })
      }
      .width('100%')
    }
    .height('100%')
  }

  getSize(index: number) {
    console.log(`Text ${index} is 更新了`); 
    return 30
  }
}

观察打印结果如下:虽然我们只改变了text的值,但是text1和text2全都刷新了
在这里插入图片描述

8.2 @Track避免冗余刷新

使用 @Track 装饰类的属性,则@Track装饰的属性变化时,只会触发该属性关联的UI更新,其他UI不更新。

class Student {
  @Track name: string
  @Track age: number

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
} 

观察打印结果如下:此时只有Text2更新了,Text1没有更新
在这里插入图片描述

注意:
● 不能在UI中使用非@Track装饰的属性,包括不能绑定在组件上、不能用于初始化子组件,错误的使用将导致JSCrash;

鸿蒙学习地址

posted @ 2025-12-12 11:47  leon_teacher  阅读(0)  评论(0)    收藏  举报