ArkTS 状态管理装饰器详解
在 ArkTS 中,UI 的刷新是由状态数据驱动的。当状态数据发生变化时,框架会识别到变化并自动更新相应的 UI 界面。为了实现这种响应式的 UI 开发,ArkTS 提供了一系列强大的装饰器。本文档将详细介绍 @State、@Provide、@Consume、@Link、@Prop 等核心装饰器的作用和使用方法。
目录
核心概念
在深入了解每个装饰器之前,需要理解 ArkTS 状态管理的基本思想:
- 数据驱动视图:UI 的显示内容完全由状态(State)数据决定。
- 声明式 UI:开发者只需要声明 UI 的最终状态,而不需要关心如何从旧状态过渡到新状态的繁琐过程。
- 单一数据源:每个状态数据都应该有其唯一的“所有者”(Source of Truth)。其他地方要么是单向接收,要么是双向同步,以避免数据混乱。
@State: 组件内部的状态
@State 是最基础也是最重要的装饰器,用于定义组件内部私有的、可变的状态。
-
作用:
- 标记一个变量为状态变量。
- 当该变量的值发生改变时,会自动触发 UI 的重新渲染,以更新与该变量相关的视图。
@State变量是其所属组件的“数据所有者”。
-
特点:
- 私有: 只能在组件内部访问和修改。
- 可变: 其值可以被改变。
- 生命周期与组件同步: 在组件创建时初始化,在组件销毁时销毁。
-
如何使用:
TypeScript
// @/main/ets/components/MyComponent.ets
@Component
struct MyComponent {
@State count: number = 0; // 使用 @State 装饰一个变量
build() {
Column() {
Text(`点击次数: ${this.count}`)
.fontSize(24)
Button('点击我')
.onClick(() => {
// 直接修改 @State 变量的值,UI 会自动更新
this.count++;
})
}
.width('100%')
.padding(20)
}
}
在上面的例子中,count 是 MyComponent 的私有状态。每次点击按钮,count 的值增加,Text 组件显示的内容也会自动更新。
@Prop: 从父组件单向同步数据
@Prop 用于组件接收来自其父组件的单向数据。
-
作用:
- 允许父组件将数据传递给子组件。
- 当父组件中对应的数据源(通常是
@State变量)发生变化时,@Prop变量的值会同步更新,并触发子组件的 UI 刷新。
-
特点:
- 单向数据流: 数据从父组件流向子组件。子组件不应该在内部直接修改
@Prop变量的值。 - 只读性: 虽然技术上可以修改,但这违背了设计原则,可能会导致应用状态不可预测。
- 单向数据流: 数据从父组件流向子组件。子组件不应该在内部直接修改
-
如何使用:
TypeScript
// 子组件: Profile.ets
@Component
struct Profile {
@Prop username: string; // 使用 @Prop 接收父组件的数据
build() {
Text(`欢迎, ${this.username}!`)
.fontSize(20)
}
}
// 父组件: UserPage.ets
@Component
struct UserPage {
@State currentUsername: string = "张三";
build() {
Column({ space: 10 }) {
// 将父组件的 @State 变量传递给子组件的 @Prop 变量
Profile({ username: this.currentUsername })
Button("切换用户")
.onClick(() => {
this.currentUsername = "李四"; // 父组件状态改变
})
}
}
}
当父组件 UserPage 中的 currentUsername 改变时,子组件 Profile 的 username 会自动更新,UI 也随之刷新。
@Link: 与父组件双向同步数据
@Link 实现了父子组件之间的双向数据同步。
-
作用:
- 允许子组件读取并修改从父组件接收的数据。
- 当子组件修改了
@Link变量,这个修改会同步回父组件的源数据,反之亦然。
-
特点:
- 双向绑定: 父子组件共享数据状态。
- 需要
$符号: 在父组件中传递@State变量时,需要在变量名前加上$符号,以传递变量的引用。
-
如何使用:
TypeScript
// 子组件: TextInput.ets
@Component
struct TextInput {
@Link textValue: string; // 使用 @Link 创建双向绑定
label: string = "输入内容";
build() {
Column() {
Text(this.label)
TextInput({ text: this.textValue })
.onChange((value: string) => {
// 子组件修改数据,会同步到父组件
this.textValue = value;
})
}
}
}
// 父组件: HomePage.ets
@Component
struct HomePage {
@State message: string = "你好";
build() {
Column({ space: 20 }) {
Text(`父组件显示的消息: ${this.message}`)
// 使用 `$` 将 @State 变量的引用传递给 @Link
TextInput({ textValue: $message, label: '编辑消息' })
}
.padding(20)
}
}
在 HomePage 中,TextInput 子组件对 textValue 的任何修改都会立刻反映在 HomePage 的 message 状态上,并更新 Text 的显示。
@Provide / @Consume: 跨组件层级共享数据
当数据需要在多个层级或没有直接父子关系的组件之间共享时,逐层传递 @Prop 或 @Link 会非常繁琐。@Provide 和 @Consume 提供了一种“依赖注入”的解决方案。
-
作用:
@Provide: 在祖先组件中“提供”一个可被其所有后代组件访问的数据。@Consume: 在后代组件中“消费”(即订阅和访问)这个被提供的数据。
-
特点:
- 跨层级: 无需手动逐层传递。
- 别名可选: 可以为
Provide的数据指定一个别名,Consume时使用该别名。 - 双向绑定:
@Provide和@Consume之间是双向绑定的。任何一方的修改都会同步给另一方。
-
如何使用:
TypeScript
// 祖先组件: AppRoot.ets
@Component
struct AppRoot {
// 在顶层组件提供一个值
@Provide('appThemeColor') themeColor: string = '#FFFFFF';
build() {
Column() {
// ... 其他组件
Level1()
}
.width('100%')
.height('100%')
.backgroundColor(this.themeColor) // 自己也可以使用
}
}
// 中间层组件: Level1.ets
@Component
struct Level1 {
build() {
Level2() // 直接渲染下一层,无需传递 props
}
}
// 后代组件: ThemedButton.ets
@Component
struct ThemedButton {
// 通过别名 'appThemeColor' 消费数据
@Consume('appThemeColor') color: string;
build() {
Button('变色按钮')
.fontColor(this.color === '#FFFFFF' ? '#000000' : '#FFFFFF')
.backgroundColor(this.color)
.onClick(() => {
// 修改 @Consume 变量会同步回 @Provide 的源头
this.color = this.color === '#FFFFFF' ? '#DDDDDD' : '#FFFFFF';
})
}
}
ThemedButton 无论嵌套多深,都可以通过 @Consume 直接获取并修改 AppRoot 中 @Provide 的 themeColor。
其他常用装饰器
@Observed 与 @ObjectLink
这对组合用于处理对象(class)的响应式。
@Observed: 标记一个类,当这个类的属性发生变化时,框架能够监听到。@ObjectLink: 在组件中持有并监听一个@Observed标记过的类实例。
TypeScript
// 1. 定义一个可被观察的类
@Observed
class UserProfile {
public name: string;
public age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
// 2. 在组件中使用
@Component
struct ProfileView {
// @ObjectLink 用于链接到 @Observed 对象
@ObjectLink user: UserProfile;
build() {
Column({ space: 10 }) {
Text(`姓名: ${this.user.name}`)
Text(`年龄: ${this.user.age}`)
Button('年龄+1')
.onClick(() => {
this.user.age++; // 直接修改对象的属性,UI 会自动更新
})
}
}
}
// 3. 父组件中创建实例并传递
@Component
struct ParentComponent {
// 数据源头是 @State
@State userProfile: UserProfile = new UserProfile("王五", 30);
build() {
ProfileView({ user: this.userProfile })
}
}
@StorageProp 与 @StorageLink
用于将组件状态与应用级的 AppStorage 或 LocalStorage 进行绑定。
AppStorage: 应用内存中的全局单例存储,生命周期与应用进程相同。LocalStorage: 设备持久化存储,应用退出后数据依然存在。@StorageProp(key): 单向绑定。当Storage中key对应的值改变时,更新组件状态。@StorageLink(key): 双向绑定。组件内对变量的修改会同步回Storage,反之亦然。
TypeScript
// 在应用启动时初始化 AppStorage
AppStorage.SetOrCreate('isUserLoggedIn', false);
@Component
struct AuthStatusView {
// 单向订阅 AppStorage 中的 'isUserLoggedIn'
@StorageProp('isUserLoggedIn') isLoggedIn: boolean = false;
build() {
Text(this.isLoggedIn ? '用户已登录' : '用户未登录')
}
}
@Component
struct LoginPage {
// 双向绑定 'isUserLoggedIn'
@StorageLink('isUserLoggedIn') loginStatus: boolean;
build() {
Button(this.loginStatus ? '退出登录' : '点击登录')
.onClick(() => {
// 修改此变量会直接更新 AppStorage,并通知所有订阅者
this.loginStatus = !this.loginStatus;
})
}
}
总结与对比
| 装饰器 | 数据所有权 | 数据流 | 主要使用场景 | 关键符号 |
|---|---|---|---|---|
@State | 组件内部 | - | 管理组件自身的、私有的状态。 | - |
@Prop | 父组件 | 单向 (父 -> 子) | 子组件接收来自父组件的不可变数据。 | - |
@Link | 父子共享 | 双向 (父 <-> 子) | 子组件需要修改父组件的状态。 | 父组件传递时使用 $ |
@Provide | 祖先组件 | 双向 | 跨多层级组件共享数据。 | - |
@Consume | 祖先组件 | 双向 | 订阅和使用祖先组件提供的数据。 | - |
@ObjectLink | 父组件 | 双向 | 在组件中引用和观察一个 @Observed 的类实例。 | - |
@StorageLink | Storage | 双向 | 将组件状态与全局或持久化存储双向绑定。 | key |
@StorageProp | Storage | 单向 | 单向订阅全局或持久化存储中的数据。 | key |