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: stringage: numbercar: Carconstructor(name: string, age: number, car: Car) {this.name = namethis.age = agethis.car = car}
}class Car {brand: stringconstructor(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 = 55this.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。步骤如下
- 使用@State装饰父组件变量,使其成为状态变量
- 使用@Prop装饰子组件变量,使其和父组件变量形成单向同步关系(父->子)
- 将父组件中的状态变量的值,通过子组件构造器传递给子组件
- 第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
子组件是不能直接更改父组件状态变量,而是采用曲线救国的策略,具体步骤如下
- 我们可以在父组件中定义一个修改父组件状态的函数
- 然后将函数传递给子组件
- 在子组件中回调父组件传递过来的函数,修改父组件的状态
如图所示:实际上也是父组件更改自己的状态
完整代码如下
@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)}
}
4.@Link 父子双向同步
子组件中被@Link装饰的变量与其父组件中对应的数据源建立双向数据绑定。也就是说,父组件可以把状态改变传给子组件;子组件也可以把状态改变传给父组件。如下图所示
实现父子双向数据绑定的具体步骤如下
- 先使用@State装饰父组件状态变量
- 使用@Link装饰子组件状态变量
- 将父组件的状态变量传递给子组件
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: stringbuild() {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步:将父组件状态变量传递给子组件
@Componentstruct 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: stringage: numberconstructor(name: string, age: number) {this.name = namethis.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: Personbuild() {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 可以实现跨层级数据的双向绑定。
如下图所示,有三个层级的组件,顶层组件数据的变化可以直接跨层级传给内层组件,内层组件的数据也可以跨层级直接传递给外层。
代码实现步骤如下
- 先试用@Provide装饰父组件状态变量
- 使用@Consum装饰子组及后代组件状态变量
5.1 顶层组件
//这是顶层组件
@Entry
@Component
struct Index {@Provide count: number = 0build() {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: numberbuild() {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: numberbuild() {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 })}
}
6. @Observe/ObjectLink 嵌套类对象属性变化
上文所述的装饰器(包括@State、@Prop、@Link、@Provide和@Consume装饰器)仅能观察到对象第一层的属性变化,但是在实际应用开发中,应用会根据开发需要,封装自己的数据模型。对于多层嵌套的情况,比如二维数组,或者数组项class,或者class的属性是class,他们的第二层的属性变化是无法观察到的。这就引出了@Observed/@ObjectLink装饰器。
6.1 观察嵌套类对象属性变化存在的问题
● 假设有如下Person和Car类;
● 如下图所示,观察当修改person.car.brand属性值时,是否会触发UI更新;
代码如下
class Person {name: stringage: numbercar: Carconstructor(name: string, age: number, car: Car) {this.name = namethis.age = agethis.car = car}
}class Car {brand: stringconstructor(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: stringage: numbercar: Carconstructor(name: string, age: number, car: Car) {this.name = namethis.age = agethis.car = car}
} @Observed
class Car {brand: stringconstructor(brand: string) {this.brand = brand}
}
❷ 将引用car.brand属性的UI抽取出来,成为自定义组件Child,并在页面中引用Chid组件,并传递this.person.car对象
@Component
struct Child {@ObjectLink car: Carbuild() {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: stringage: numbercar: Carconstructor(name: string, age: number, car: Car) {this.name = namethis.age = agethis.car = car}
}@Observed
class Car {brand: stringconstructor(brand: string) {this.brand = brand}
} @Component
struct Child {@ObjectLink car: Carbuild() {Column({ space: 10 }) {Text(`${this.car.brand}`)}}
}@Component
struct MyListItem {@ObjectLink person: Personbuild() {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: numberonCountChange(){console.log("观察到子组件count变化"+this.count)}build() {Column() {Text(`${this.count}`)}}
}//入口组件
@Entry
@Component
struct Index {@State count: number = 0build() {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: numberonCountChange() {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 = 0onCountChange() {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: stringage: numberconstructor(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: numberconstructor(name: string, age: number) {this.name = name;this.age = age;}
}
观察打印结果如下:此时只有Text2更新了,Text1没有更新
注意:
● 不能在UI中使用非@Track装饰的属性,包括不能绑定在组件上、不能用于初始化子组件,错误的使用将导致JSCrash;