赞
踩
在前文的描述中,我们构建的页面多为静态界面。如果希望构建一个动态的、有交互的界面,就需要引入“状态”的概念。我们本章节来学习状态管理机制
在声明式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
。
参考文献:
[1]OpenHarmoney应用开发文档
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。