ArkUI学习之状态管理
在声明式UI编程框架中,UI是程序状态的运行结果,用户构建了一个UI模型,其中应用的运行时的状态是参数。当参数改变时,UI作为返回结果,也将进行对应的改变。这些运行时的状态变化所带来的UI的重新渲染,在ArkUI中统称为状态管理机制。
一、装饰器总览
ArkUI提供了多种装饰器,根据状态变量的影响范围,将所有的装饰器可以大致分为:
- 管理组件拥有状态的装饰器:组件级别的状态管理,可以观察组件内变化,和不同组件层级的变化,但需要同一个页面内。
- 管理应用拥有状态的装饰器:应用级别的状态管理,可以观察不同页面,甚至不同UIAbility的状态变化,是应用内全局的状态管理。
Components部分的装饰器为组件级别的状态管理,Application部分为应用的状态管理。
管理组件拥有的状态,即图中Components级别的状态管理:
@State
:@State装饰的变量拥有其所属组件的状态,可以作为其子组件单向和双向同步的数据源。当其数值改变时,会引起相关组件的渲染刷新。@Prop
:@Prop装饰的变量可以和父组件建立单向同步关系,@Prop装饰的变量是可变的,但修改不会同步回父组件。@Link
:@Link装饰的变量和父组件构建双向同步关系的状态变量,父组件会接受来自@Link装饰的变量的修改的同步,父组件的更新也会同步给@Link装饰的变量。@Provide/@Consume
:@Provide/@Consume装饰的变量用于跨组件层级(多层组件)同步状态变量,可以不需要通过参数命名机制传递,通过alias(别名)或者属性名绑定。@Observed
:@Observed装饰class,需要观察多层嵌套场景的class需要被@Observed装饰。单独使用@Observed没有任何作用,需要和@ObjectLink、@Prop连用。@ObjectLink
:@ObjectLink装饰的变量接收@Observed装饰的class的实例,应用于观察多层嵌套场景,和父组件的数据源构建双向同步。
管理应用拥有的状态,即图中Application级别的状态管理:
LocalStorage
:页面级UI状态存储,通常用于UIAbility内、页面间的状态共享。AppStorage
:特殊的单例LocalStorage对象,由UI框架在应用程序启动时创建,为应用程序UI状态属性提供中央存储。PersistentStorage
:持久化存储UI状态,通常和AppStorage配合使用,选择AppStorage存储的数据写入磁盘,以确保这些属性在应用程序重新启动时的值与应用程序关闭时的值相同。Environment
:应用程序运行的设备的环境参数,环境参数会同步到AppStorage中,可以和AppStorage搭配使用。
其他状态管理功能
@Watch
用于监听状态变量的变化。$$运算符
:给内置组件提供TS变量的引用,使得TS变量和内置组件的内部状态保持同步。- @Track用于class对象属性级更新
二、管理组件拥有的状态
@State装饰器:组件内状态
@State装饰的变量,或称为状态变量,一旦变量拥有了状态属性,就和自定义组件的渲染绑定起来。当状态改变时,UI会发生对应的渲染改变。
@State装饰的变量拥有以下特点:
- @State装饰的变量与子组件中的@Prop装饰变量之间建立单向数据同步,与@Link、@ObjectLink装饰变量之间建立双向数据同步。
- @State装饰的变量生命周期与其所属自定义组件的生命周期相同。
- 支持多种数据类型:允许 Object、class、string、number、boolean、enum、Date类型,以及这些类型的数组。
- 内部私有:标记为 @State 的属性是私有变量,只能在组件内访问。
- 支持多个实例:组件不同实例的内部状态数据独立。
- 需要本地初始化:必须为所有 @State 变量分配初始值,将变量保持未初始化可能导致框架行为未定义,初始值需要是有意义的值,比如设置
class
类型的值为null
就是无意义的,会导致编译报错。 - 创建自定义组件时支持通过状态变量名设置初始值:在创建组件实例时,可以通过变量名显式指定 @State 状态属性的初始值。
装饰class对象类型的变量 当装饰的数据类型为class或者Object时,可以观察到自身的赋值的变化,和其属性赋值的变化,即Object.keys(observedObject)返回的所有属性
创建一个Model对象
class Model { public value: string; constructor(value: string) { this.value = value; } }
在父组件中初始化@State装饰对象,父组件初始化将会覆盖本地初始化。
@Entry @Component struct EntryComponent { build() { Column() { // 此处指定的参数都将在初始渲染时覆盖本地定义的默认值,并不是所有的参数都需要从父组件初始化 MyComponent({ count: 1, increaseBy: 2 }) .width(300) MyComponent({ title: new Model('Hello World 2'), count: 7 }) } } }
在本地初始化@State装饰对象,@State变量更新会触发组件UI更新
@Component struct MyComponent { @State title: Model = new Model('Hello World'); @State count: number = 0; private increaseBy: number = 1; build() { Column() { Text(`${this.title.value}`) .margin(10) Button(`Click to change title`) .onClick(() => { // @State变量的更新将触发上面的Text组件内容更新 this.title.value = this.title.value === 'Hello ArkUI' ? 'Hello World' : 'Hello ArkUI'; }) .width(300) .margin(10) Button(`Click to increase count = ${this.count}`) .onClick(() => { // @State变量的更新将触发该Button组件的内容更新 this.count += this.increaseBy; }) .width(300) .margin(10) } } }
@Prop装饰器:父子单向同步
@Prop装饰的变量可以和父组件建立单向的同步关系。@Prop装饰的变量是可变的,但是变化不会同步回其父组件。
@Prop装饰的变量拥有以下特点:
- 支持简单数据类型:仅支持
number
、string
、boolean
、Object
、class
、enum
类型; - 内部私有:标记为
@Prop
的属性是私有变量,只能在组件内访问。 - 支持多个实例:组件不同实例的内部状态数据独立。
- @Prop装饰器不能在@Entry装饰的自定义组件中使用。
@Component struct CountDownComponent { @Prop count: number = 0; costOfOneAttempt: number = 1; build() { Column() { if (this.count > 0) { Text(`You have ${this.count} Nuggets left`) } else { Text('Game over!') } // @Prop装饰的变量不会同步给父组件 Button(`Try again`).onClick(() => { this.count -= this.costOfOneAttempt; }) } } } @Entry @Component struct ParentComponent { @State countDownStartValue: number = 10; build() { Column() { Text(`Grant ${this.countDownStartValue} nuggets to play.`) // 父组件的数据源的修改会同步给子组件 Button(`+1 - Nuggets in New Game`).onClick(() => { this.countDownStartValue += 1; }) // 父组件的修改会同步给子组件 Button(`-1 - Nuggets in New Game`).onClick(() => { this.countDownStartValue -= 1; }) CountDownComponent({ count: this.countDownStartValue, costOfOneAttempt: 2 }) } } }
@Link装饰器:父子双向同步
子组件中被@Link装饰的变量与其父组件中对应的数据源建立双向数据绑定。@Link装饰的变量与其父组件中的数据源共享相同的值。
@Link装饰的变量拥有以下特点:
- Object、class、string、number、boolean、enum类型,以及这些类型的数组。支持Date类型。
- 内部私有:标记为
@Link
的属性是私有变量,只能在组件内访问。 - 支持多个实例:组件不同实例的内部状态数据独立。
- 不支持内部初始化:在创建组件的新实例时,必须将值传递给
@Link
修饰的变量进行初始化,不支持在组件内部进行初始化。
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) .height(40) .backgroundColor('#64bb5c') .fontColor('#FFFFFF,90%') .onClick(() => { if (this.greenButtonState.width < 700) { // 更新class的属性,变化可以被观察到同步回父组件 this.greenButtonState.width += 60; } else { // 更新class,变化可以被观察到同步回父组件 this.greenButtonState = new GreenButtonState(180); } }) } } @Component struct YellowButton { @Link yellowButtonState: number; build() { Button('Yellow Button') .width(this.yellowButtonState) .height(40) .backgroundColor('#f7ce00') .fontColor('#FFFFFF,90%') .onClick(() => { // 子组件的简单类型可以同步回父组件 this.yellowButtonState += 40.0; }) } } @Entry @Component struct ShufflingContainer { @State greenButtonState: GreenButtonState = new GreenButtonState(180); @State yellowButtonProp: number = 180; build() { Column() { Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center }) { // 简单类型从父组件@State向子组件@Link数据同步 Button('Parent View: Set yellowButton') .width(312) .height(40) .margin(12) .fontColor('#FFFFFF,90%') .onClick(() => { this.yellowButtonProp = (this.yellowButtonProp < 700) ? this.yellowButtonProp + 40 : 100; }) // class类型从父组件@State向子组件@Link数据同步 Button('Parent View: Set GreenButton') .width(312) .height(40) .margin(12) .fontColor('#FFFFFF,90%') .onClick(() => { this.greenButtonState.width = (this.greenButtonState.width < 700) ? this.greenButtonState.width + 100 : 100; }) // class类型初始化@Link GreenButton({ greenButtonState: $greenButtonState }).margin(12) // 简单类型初始化@Link YellowButton({ yellowButtonState: $yellowButtonProp }).margin(12) } } } }
1.点击子组件GreenButton和YellowButton中的Button,子组件会发生相应变化,将变化同步给父组件。因为@Link是双向同步,会将变化同步给@State。
2.当点击父组件ShufflingContainer中的Button时,@State变化,也会同步给@Link,子组件也会发生对应的刷新。
管理应用拥有的状态
LocalStorage:页面级UI状态存储
LocalStorage是页面级的UI状态存储,通过@Entry装饰器接收的参数可以在页面内共享同一个LocalStorage实例。LocalStorage支持UIAbility实例内多个页面间状态共享。 LocalStorage是ArkTS为构建页面级别状态变量提供存储的内存内“数据库”。
- 应用程序可以创建多个LocalStorage实例,LocalStorage实例可以在页面内共享,也可以通过GetShared接口,实现跨页面、UIAbility实例内共享。
- 组件树的根节点,即被@Entry装饰的@Component,可以被分配一个LocalStorage实例,此组件的所有子组件实例将自动获得对该LocalStorage实例的访问权限。
- 被@Component装饰的组件最多可以访问一个LocalStorage实例和AppStorage,未被@Entry装饰的组件不可被独立分配LocalStorage实例,只能接受父组件通过@Entry传递来的LocalStorage实例。一个LocalStorage实例在组件树上可以被分配给多个组件。
- LocalStorage中的所有属性都是可变的。
LocalStorage根据与@Component装饰的组件的同步类型不同,提供了两个装饰器:
- @LocalStorageProp:@LocalStorageProp装饰的变量和与LocalStorage中给定属性建立单向同步关系。
- @LocalStorageLink:@LocalStorageLink装饰的变量和在@Component中创建与LocalStorage中给定属性建立双向同步关系。
AppStorage:应用全局的UI状态存储
AppStorage是应用全局的UI状态存储,是和应用的进程绑定的,由UI框架在应用程序启动时创建,为应用程序UI状态属性提供中央存储。
和AppStorage不同的是,LocalStorage是页面级的,通常应用于页面内的数据共享。而AppStorage是应用级的全局状态共享,还相当于整个应用的“中枢”,持久化数据PersistentStorage和环境变量Environment都是通过AppStorage中转,才可以和UI交互。
@StorageProp
- 单向同步:从AppStorage的对应属性到组件的状态变量。
- Object、 class、string、number、boolean、enum类型,以及这些类型的数组。
- 组件本地的修改是允许的,但是AppStorage中给定的属性一旦发生变化,将覆盖本地的修改。
- @StorageProp不支持从父节点初始化,只能AppStorage中key对应的属性初始化,如果没有对应key的话,将使用本地默认值初始化。
@StorageLink
@StorageLink(key)
装饰的变量是组件内部的状态数据,当这些状态数据被修改时,将会调用所在组件的 build()
方法进行UI刷新。组件通过使用 @StorageLink(key)
装饰的状态变量与 AppStorage
建立双向数据绑定。
- 支持多种数据类型:支持的数据类型和
@State
一致且支持object
。 - 需要本地初始化:必须为所有
@StorageLink
变量分配初始值。 - 数据状态全局化:使用
@StorageLink
修饰的数据变化后全局都会改变。 - 数据持久化:通过搭配
PersistentStorage
接口实现数据持久化。
@Entry @Component struct ComponentTest { @StorageLink('time') time: string = "1648643734154";// 使用StorageLink标记并初始化 build() { Column({space: 10}) { Text(`父组件【${this.time}】`) // 使用time值 .fontSize(20) .backgroundColor(Color.Pink) Button('更新时间') .onClick(() => { this.time = new Date().getTime().toString();// 更改time的值 }) } .width('100%') .height('100%') .padding(10) } }
其他状态管理
@Watch装饰器:状态变量更改通知
@Watch
用来监听状态变量的变化,当它修饰的状态变量发生变更时,回调相应的方式,
- 当观察到状态变量的变化的时候,对应的@Watch的回调方法将被触发;
- @Watch方法在自定义组件的属性变更之后同步执行;
- 如果在@Watch的方法里改变了其他的状态变量,也会引起状态变更和@Watch的执行;
- 在第一次初始化的时候,@Watch装饰的方法不会被调用,即认为初始化不是状态变量的改变。只有在后续状态改变时,才会调用@Watch回调方法。
- 为了避免循环的产生,建议不要在@Watch的回调方法里修改当前装饰的状态变量;
- 不建议在@Watch函数中调用async await,异步行为可能会导致重新渲染速度的性能问题。
//给状态变量 `count` 增加一个 `@Watch` 装饰器,通过 `@Watch` 注册一个回调方法 `function_name` @State @Watch("function_name") count : number = 0; //当状态变量 `count` 被改变时, 触发 `function_name` 回调。 function_name(propName: string): void {}
⏰ @Watch和自定义组件更新示例:
@Component struct TotalView { @Prop @Watch('onCountUpdated') count: number = 0; @State total: number = 0; // @Watch 回调 onCountUpdated(propName: string): void { this.total += this.count; } build() { Text(`Total: ${this.total}`) } } @Entry @Component struct CountModifier { @State count: number = 0; build() { Column() { Button('add to basket') .onClick(() => { this.count++ }) TotalView({ count: this.count }) } } }
$$语法:内置组件双向同步
$$
运算符为系统内置组件(比如TextInput等)提供TS变量的引用,使得TS变量和系统内置组件的内部状态保持同步。
⏰ 以
TextInput
方法的text参数为例:
@Entry @Component struct TextInputExample { /** 状态变量 */ @State text: string = '' /** 普通变量 */ private controller: TextInputController = new TextInputController() /** 构建函数 */ build() { Column({ space: 20 }) { Text(this.text) TextInput({ text: $$this.text, placeholder: 'input your word...', controller: this.controller }) .placeholderColor(Color.Grey) .placeholderFont({ size: 14, weight: 400 }) .caretColor(Color.Blue) .width(300) }.width('100%').height('100%').justifyContent(FlexAlign.Center) } }
@Track装饰器:class对象属性级更新
@Track应用于class对象的属性级更新。@Track装饰的属性变化时,只会触发该属性关联的UI更新。
🔊: @Track是class对象的属性装饰器。当一个class对象是状态变量时,@Track装饰的属性发生变化,只会触发该属性关联的UI更新;而未被标记的属性不能在UI中使用。就是说@Track不像@Observe 那样所有的属性都包装成状态变量,@Track操作更加精细, 只针对部分属性。
⏰ 观察变化和行为表现
class LogTack { @Track public str1: string; @Track public str2: string; constructor(str1: string) { this.str1 = str1; this.str2 = 'World'; } } class LogNotTrack { public str1: string; public str2: string; constructor(str1: string) { this.str1 = str1 this.str2 = '世界' } } @Entry @Component struct Index { /** 状态呢变量 */ @State logTrack: LogTack = new LogTack('Hello') @State logNotTrack: LogNotTrack = new LogNotTrack('你好') /** 自定义函数 */ isRender(index: number) { console.log(`Text ${index} is rendered`); return 50; } /** 构建函数 */ build() { Row() { Column({space:30}) { Column() { Text(this.logTrack.str1) .fontSize(this.isRender(1)) .fontWeight(FontWeight.Bold) Text(this.logTrack.str2) .fontSize(this.isRender(2)) .fontWeight(FontWeight.Bold) Button('change logTrack.str1') .onClick(() => { this.logTrack.str1 = 'Bye' }) } Column() { Text(this.logNotTrack.str1) .fontSize(this.isRender(3)) .fontWeight(FontWeight.Bold) Text(this.logNotTrack.str2) .fontSize(this.isRender(4)) .fontWeight(FontWeight.Bold) Button('change logNotTrack.str1') .onClick(() => { this.logNotTrack.str1 = '再见' }) } }.width('100%') }.height('100%') } }
结果:

在上面的示例中:
-
类LogTrack中的属性均被@Track装饰器装饰,点击按钮"change logTrack.str1",此时UINode1刷新,UINode2不刷新,只有一条日志输出,避免了冗余刷新。
Text 1 is rendered
-
类logNotTrack中的属性均未被@Track装饰器装饰,点击按钮"change logNotTrack.str1",此时UINode3、UINode4均会刷新,有两条日志输出,存在冗余刷新。
Text 3 is rendered Text 4 is rendered
使用@Track装饰器可以避免冗余刷新。
⏰ 限制条件
-
不能在UI中使用非@Track装饰的属性,包括不能绑定在组件上、不能用于初始化子组件,错误的使用将导致JSCrash;可以在非UI中使用非@Track装饰的属性,如事件回调函数中、生命周期函数中等。
-
建议开发者不要混用包含@Track的class对象和不包含@Track的class对象,如联合类型中、类继承中等。
⏰ 使用场景
以下示例展示组件更新和@Track的处理步骤。对象log是@State装饰的状态变量,logInfo是@Track的成员属性,其余成员属性都是非@Track装饰的,而且也不准备在UI中更新它们的值。
class Log { @Track public logInfo: string; public owner: string; public id: number; public time: Date; public location: string; public reason: string; constructor(logInfo: string) { this.logInfo = logInfo this.owner = 'OH' this.id = 0 this.time = new Date() this.location = 'CN' this.reason = 'NULL' } } @Entry @Component struct Index { /** 状态变量 */ @State log: Log = new Log('origin info.') /** 构建函数 */ build() { Row() { Column() { Text(this.log.logInfo) .width('100%') .fontSize(50) .fontWeight(FontWeight.Bold) .textAlign(TextAlign.Center) .onClick(() => { // The properties without @Track can be used in the event handler. console.log('owner: ' + this.log.owner + ' id: ' + this.log.id + ' time: ' + this.log.time + ' location: ' + this.log.location + ' reason: ' + this.log.reason); this.log.time = new Date(); this.log.id++; this.log.logInfo += ' info.'; }) }.width('100%') }.height('100%') } }
结果:

处理步骤:
-
AddLog自定义组件的Text.onClick点击事件自增字符串' info.'。
-
由于@State log变量的@Track属性logInfo更改,Text重新渲染。