在前文的描述中,我们构建的页面多为静态界面。如果希望构建一个动态的、有交互的界面,就需要引入“状态”的概念。我们本章节来学习状态管理机制
在声明式UI编程框架中,UI是程序状态的运行结果,用户构建了一个UI模型,其中应用的运行时的状态是参数。当参数改变时,UI作为返回结果,也将进行对应的改变。这些运行时的状态变化所带来的UI的重新渲染,在ArkUI中统称为状态管理机制
。
自定义组件拥有变量,变量必须被装饰器装饰
才可以成为状态变量
,状态变量的改变会引起UI的渲染刷新。如果不使用状态变量,UI只能在初始化时渲染,后续将不会再刷新。 下图展示了State和View(UI)之间的关系
。
View(UI):UI渲染
,指将build方法
内的UI描述
和@Builder装饰的方法内的UI描述
映射到界面。State:状态
,指驱动UI更新的数据。用户通过触发组件的事件方法,改变状态数据。状态数据的改变,引起UI的重新渲染
。
@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 })
}
}
}
状态变量:被状态装饰器装饰的变量,状态变量值的改变会引起UI的渲染更新
。示例中:@State num: number = 1
,其中,@State
是状态装饰器,num
是状态变量。
常规变量:不会引起UI的刷新
,示例中increaseBy
变量为常规变量
。
从父组件初始化:父组件使用命名参数机制,将指定参数传递给子组件。子组件初始化的默认值在有父组件传值的情况下,会被覆盖
。
初始化子节点:父组件中状态变量可以传递给子组件
,初始化子组件对应的状态变量。
根据状态变量的影响范围,将所有的装饰器可以大致分为管理组件拥有状态的装饰器
和管理应用拥有状态的装饰器
管理组件拥有状态的装饰器
:组件级别的状态管理
,可以观察组件内变化,和不同组件层级的变化,但需要唯一观察同一个组件树上,即同一个页面内。
管理应用拥有状态的装饰器
:应用级别的状态管理
,可以观察不同页面,甚至不同UIAbility的状态变化,是应用内全局的状态管理。
上图中,Components部分的装饰器为组件级别的状态管理
,Application部分为应用的状态管理
。开发者可以通过@StorageLink/@LocalStorageLink
实现应用和组件状态的双向同步
,通过@StorageProp/@LocalStorageProp
实现应用和组件状态的单向同步
。
@State
装饰的变量,或称为状态变量
,组件内状态
,一旦变量拥有了状态属性,就和自定义组件的渲染绑定起来。当状态改变时,UI会发生对应的渲染改变
。与声明式范式中的其他被装饰变量一样,是私有的
,只能从组件内部访问
,声明时必须指定其类型和本地初始化
。初始化也可选择使用命名参数机制从父组件完成初始化
。
@State
装饰的变量与子组件中的@Prop
装饰变量之间建立单向数据同步
,与@Link
、@ObjectLink
装饰变量之间建立双向数据同步
。@State
装饰的变量生命周期
与其所属自定义组件的生命周期相同。any
,不支持简单类型和复杂类型的联合类型
,不允许使用undefined
和null
。Length
、ResourceStr
、ResourceColor
类型,Length
、ResourceStr
、ResourceColor
为简单类型和复杂类型的联合类型
。并不是状态变量的所有更改都会引起UI的刷新,只有可以被框架观察到的修改才会引起UI刷新。该小节去介绍什么样的修改才能被观察到,以及观察到变化后,框架的是怎么引起UI刷新的,即框架的行为表现是什么。
boolean
、string
、number
类型时,可以观察到数值的变化。class
或Object
时,可以观察到自身和其属性
赋值的变化,即Object.keys(observedObject)
返回的所有属性。简单理解为类一级属性可以观察到变化
。array
时,可以观察到数组本身的赋值
和添加
、删除
、更新数组
的变化。但无法观察array中元素内的属性
。Date
时,可以观察到Date整体的赋值,同时可通过调用Date的setxxx()方法
来更新Date
的属性。当状态变量被改变时,查询依赖该状态变量的组件;执行依赖该状态变量的组件的更新方法,组件更新渲染;和该状态变量不相关的组件或者UI描述不会发生重新渲染,从而实现页面渲染的按需更新。
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%')
}
}
@Prop装饰的变量实现父子组件单向同步
,与父组件建立单向
的同步关系。@Prop装饰的变量是可变的,但是变化不会同步回其父组件。
数据源改变会更新@Prop变量
@Prop变量可以本地修改,但不会同步给数据源
深拷贝
,在拷贝的过程中除了基本类型、Map、Set、Date、Array外,都会丢失类型。不能在@Entry装饰的入口组件中使用
单向同步
@Prop
装饰的变量和@State
以及其他装饰器
同步时双方的类型必须相同
基本和5.1.4 @State的观察变化相同,这里列举下不同点:
@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变量。
// 以下是嵌套类对象的数据结构。
@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)
}
}
@Observed装饰
,且每一层都要被@Prop
接收,这样才能观察到嵌套。@Observed装饰
只能使观察的值往下传,这个地方难以说清楚,看上面Child和Child1的案例表现。子组件中被@Link
装饰的变量与其父组件中对应的数据源建立双向数据绑定
,实现组件间父子双向同步
。@Link装饰的变量与其父组件中的数据源共享相同的值。但需要注意@Link装饰器不能在@Entry装饰的自定义组件中使用
。
基本和5.1.4 @State的观察变化相同
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
等装饰器装饰的状态变量
。
@Provide和@Consume,应用于与后代组件的双向数据同步
,应用于状态数据在多个层级之间传递的场景。不同于上文提到的父子组件之间通过命名参数机制传递,@Provide和@Consume摆脱参数传递机制的束缚,实现跨层级传递
。
@Provide
装饰的变量是在祖先组件中
,可以理解为被“提供”
给后代的状态变量。@Consume
装饰的变量是在后代组件中
,去“消费(绑定)”
祖先组件提供的变量。@Provide
和@Consume
可以通过相同的变量名
或者相同的变量别名
绑定,建议类型相同
,@Provide
修饰的变量和@Consume
修饰的变量是一对多
的关系。
@State
的规则同样适用于@Provide
,差异为@Provide
还作为多层后代的同步源
。
基本和5.1.4 @State的观察变化相同,我们直接看实例
@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
的指向。
上文所述的装饰器仅能观察到第一层的变化,但是在实际应用开发中,应用会根据开发需要,封装自己的数据模型。对于多层嵌套的情况,比如二维数组,或者数组项class,或者class的属性是class,他们的第二层的属性变化是无法观察到的。这就引出了@Observed/@ObjectLink
装饰器。
@ObjectLink和@Observed
类装饰器用于在涉及嵌套对象或数组
的场景中进行双向数据同步
@Observed类装饰器
:装饰class
。需要放在class的定义前,使用new创建类对象。
@ObjectLink变量装饰器
:必须为被@Observed
装饰的class
实例,必须指定类型。不支持简单类型,可以使用@Prop
。@ObjectLink的变量只读,但变量的属性可变
。@ObjectLink
装饰的变量不能被赋值,如果要使用赋值操作,请使用@Prop
。