本文献给:
已掌握基础 UI 构建、能独立完成静态页面布局的鸿蒙开发者。本文将深入讲解 HarmonyOS ArkUI 中的状态管理 V1 版本,聚焦 @State、@Prop、@Link 三者的使用方式、区别与适用场景,并介绍 AppStorage 全局状态存储的基本用法,帮助你构建响应式、可交互的应用界面。
你将学到:
@State装饰器的用法与组件内部状态管理@Prop实现父子组件间的单向数据传递@Link实现父子组件间的双向数据同步@State、@Prop、@Link的区别与选型策略AppStorage的基本 API 与跨页面/跨组件共享状态- 实际场景中状态管理的综合应用
目录
- [一、@State ------ 组件内部状态](#一、@State —— 组件内部状态)
-
- [1.1 基本语法](#1.1 基本语法)
- [1.2 @State 的特点](#1.2 @State 的特点)
- [1.3 注意事项](#1.3 注意事项)
- [二、@Prop ------ 父到子单向传递](#二、@Prop —— 父到子单向传递)
-
- [2.1 基本语法](#2.1 基本语法)
- [2.2 @Prop 的特点](#2.2 @Prop 的特点)
- [2.3 注意事项](#2.3 注意事项)
- [三、@Link ------ 父子双向同步](#三、@Link —— 父子双向同步)
-
- [3.1 基本语法](#3.1 基本语法)
- [3.2 @Link 的特点](#3.2 @Link 的特点)
- [3.3 注意事项](#3.3 注意事项)
- [四、@State、@Prop、@Link 三者对比与选型](#四、@State、@Prop、@Link 三者对比与选型)
- [五、AppStorage ------ 全局状态存储](#五、AppStorage —— 全局状态存储)
-
- [5.1 基本 API](#5.1 基本 API)
- [5.2 存储与使用](#5.2 存储与使用)
- [5.3 应用场景](#5.3 应用场景)
- [六、综合示例 ------ 计数器与用户信息联动](#六、综合示例 —— 计数器与用户信息联动)
- 七、常见错误与注意事项
-
- [7.1 直接修改 @State 对象的属性不触发更新](#7.1 直接修改 @State 对象的属性不触发更新)
- [7.2 @Prop 变量被当成双向绑定使用](#7.2 @Prop 变量被当成双向绑定使用)
- [7.3 @Link 使用时忘记在父组件中使用 `\` 传引用](#7.3 @Link 使用时忘记在父组件中使用 `` 传引用)
- [7.4 AppStorage 未初始化就访问](#7.4 AppStorage 未初始化就访问)
- [7.5 滥用 AppStorage](#7.5 滥用 AppStorage)
- 八、小结
一、@State ------ 组件内部状态
@State 用于声明组件内部的私有状态变量。当被 @State 装饰的变量值发生变化时,ArkUI 会自动触发该组件的 build 方法重新执行,从而实现 UI 的更新。
1.1 基本语法
typescript
@Entry
@Component
struct Counter {
@State count: number = 0;
build() {
Column({ space: 20 }) {
Text(`当前计数:${this.count}`)
.fontSize(24)
Button('增加')
.onClick(() => {
this.count++; // 修改 @State 变量,UI 自动刷新
})
}
.width('100%')
.padding(20)
}
}
@State只能装饰组件内部定义的变量,不能用于从外部传入的属性。- 变量的变化必须是可观察的 (如赋值、数组/对象整体替换等),简单的属性修改(如
this.obj.prop = 'new')需要配合@Observed/@ObjectLink才能触发更新(V2 版本中更完善,本文只讨论 V1 基础场景)。
1.2 @State 的特点
| 特性 | 说明 |
|---|---|
| 所属范围 | 组件私有状态 |
| 触发更新 | 变量重新赋值时,触发所在组件 build 重新执行 |
| 数据流向 | 组件内部自管理,外部不可直接修改 |
| 适用场景 | 本地开关、计数器、表单输入等独立状态 |
1.3 注意事项
@State变量必须在声明时或在构造函数中初始化。- 对于复杂类型(对象、数组),若需深度观察属性变化,建议使用
@Observed与@ObjectLink(V2 特性),或通过整体替换对象/数组来触发刷新。
typescript
@State user: { name: string, age: number } = { name: 'Alice', age: 25 };
// 错误:直接修改属性不会触发更新
this.user.age = 26;
// 正确:整体替换
this.user = { ...this.user, age: 26 };
二、@Prop ------ 父到子单向传递
@Prop 用于接收父组件传递过来的数据,并在子组件内部作为一份只读副本 使用。父组件修改数据时,会同步更新子组件;但子组件不能修改 @Prop 的值(修改不会回传给父组件,且子组件自身视图可能更新但不影响父组件状态)。
2.1 基本语法
父组件:
typescript
@Entry
@Component
struct Parent {
@State parentMessage: string = '来自父组件的消息';
build() {
Column({ space: 20 }) {
Text(`父组件:${this.parentMessage}`)
Button('修改消息').onClick(() => {
this.parentMessage = '已更新的消息';
})
// 向子组件传递 @State 变量
Child({ childMsg: this.parentMessage })
}
.width('100%')
.padding(20)
}
}
子组件:
typescript
@Component
struct Child {
@Prop childMsg: string; // 接收父组件传入的值
build() {
Text(`子组件收到:${this.childMsg}`)
.fontColor(Color.Gray)
}
}
运行后,点击父组件的按钮,parentMessage 变化,子组件内的 childMsg 自动同步刷新。
2.2 @Prop 的特点
| 特性 | 说明 |
|---|---|
| 数据来源 | 父组件传入 |
| 修改行为 | 子组件可修改本地副本,但不会反向影响父组件 |
| 同步方式 | 父组件源数据变化时,子组件副本自动同步 |
| 适用场景 | 展示型子组件,仅需要读取数据,无需回传 |
2.3 注意事项
@Prop变量不能 在子组件内部通过this.prop = newValue去期望父组件更新。@Prop支持的类型与@State类似,包括基础类型和复杂类型。- 如果想实现子组件修改数据并同步回父组件,应使用
@Link。
三、@Link ------ 父子双向同步
@Link 在 @Prop 的基础上实现了双向绑定 。子组件通过 @Link 获取父组件的状态引用,子组件对变量的任何修改都会直接反映到父组件,反之亦然。
3.1 基本语法
父组件:
typescript
@Entry
@Component
struct Parent {
@State isOn: boolean = false;
build() {
Column({ space: 20 }) {
Text(`父组件开关状态:${this.isOn}`)
// 传递时使用 $ 符号创建引用
ToggleSwitch({ switchState: $isOn })
}
.width('100%')
.padding(20)
}
}
子组件(开关):
typescript
@Component
struct ToggleSwitch {
@Link switchState: boolean; // 双向绑定引用
build() {
Row() {
Text('开关:')
Toggle({ type: ToggleType.Switch, isOn: this.switchState })
.onChange((isOn: boolean) => {
this.switchState = isOn; // 修改会直接更新父组件
})
}
}
}
注意父组件传值时使用了 $isOn 语法,表示传递的是引用而非值的副本。
3.2 @Link 的特点
| 特性 | 说明 |
|---|---|
| 数据来源 | 父组件 @State(或 @Link)变量的引用 |
| 修改行为 | 子组件修改立即同步到父组件,父组件修改也同步到子组件 |
| 适用场景 | 表单控件、开关、输入框等需要父子数据一致的组件 |
3.3 注意事项
@Link装饰的变量必须从父组件通过$传递引用,不能直接赋值字面量。- 如果数据需要在多个层级间传递,可以逐层使用
@Link,但嵌套过深时建议考虑AppStorage或状态管理库。 @Link变量不能设置默认值,其初始值完全由父组件提供。
四、@State、@Prop、@Link 三者对比与选型
| 装饰器 | 数据归属 | 数据流向 | 修改影响范围 | 典型场景 |
|---|---|---|---|---|
@State |
组件自身 | 内部循环 | 自身 UI 刷新 | 本地状态(计数器等) |
@Prop |
子组件(副本) | 父 → 子(单向) | 仅子组件本地副本 | 展示型子组件(标签等) |
@Link |
父组件(引用) | 父 ↔ 子(双向) | 父子组件同步变化 | 输入控件、开关、滑块等 |
选型建议:
- 如果状态仅当前组件使用,用
@State。 - 子组件只需展示数据,不需要修改,用
@Prop。 - 子组件需要修改并同步回父组件,用
@Link。 - 深层嵌套或跨页面共享时,使用
AppStorage或更高级的状态管理方案。
五、AppStorage ------ 全局状态存储
当应用需要在不同页面、不同组件之间 共享状态时,@State / @Prop / @Link 的组件树传递方式会变得繁琐。AppStorage 提供了一种全局的键值对存储机制,任何组件都可以读写其中的数据,且数据的变更会自动通知所有绑定者刷新 UI。
5.1 基本 API
AppStorage.setOrCreate<T>(key: string, value: T):创建或更新一个全局状态值。AppStorage.link<T>(key: string):返回双向绑定的引用(类似@Link),用于组件中修改会同步回 AppStorage。AppStorage.prop<T>(key: string):返回单向同步的值(类似@Prop),用于只读展示。
5.2 存储与使用
在某处(如入口 Ability 或页面)设置初始值:
typescript
import AppStorage from '@ohos.app.ability.AppStorage';
// 在页面初始化时设置
onPageShow() {
AppStorage.setOrCreate('username', 'Alice');
}
在任意组件中双向绑定:
typescript
@Component
struct UserInfo {
// 通过 @StorageLink 装饰器直接绑定 AppStorage 中的值(双向)
@StorageLink('username') username: string = '';
build() {
Column() {
Text(`当前用户:${this.username}`)
TextInput({ placeholder: '修改用户名', text: this.username })
.onChange((value: string) => {
this.username = value; // 修改会直接更新 AppStorage,并同步到所有绑定该 key 的组件
})
}
}
}
注意:
@StorageLink需要配合@Entry或自定义组件使用,且key需与 AppStorage 中的一致。
单向读取(只展示):
typescript
@Component
struct Greeting {
@StorageProp('username') username: string = 'Guest';
build() {
Text(`你好,${this.username}`)
}
}
@StorageProp:单向同步,不能通过组件修改 AppStorage 中的值。@StorageLink:双向同步,修改组件变量会回写 AppStorage。
5.3 应用场景
- 用户登录信息(用户名、token 等)
- 全局配置(主题色、字体大小)
- 跨页面数据共享(购物车数量等)
注意:不要滥用 AppStorage 存储大量复杂状态,避免造成数据流难以追踪。建议只用于真正的全局共享状态。
六、综合示例 ------ 计数器与用户信息联动
下面通过一个完整的示例,综合运用 @State、@Prop、@Link 和 AppStorage。
场景 :页面顶部显示全局用户名(可从设置页修改),中间有一个计数器(本地状态),底部有一个开关控制是否显示详细信息,开关状态通过 @Link 在父子组件间双向同步。
typescript
import AppStorage from '@ohos.app.ability.AppStorage';
// 假设在应用启动时已设置 username
// AppStorage.setOrCreate('username', 'Alice');
@Entry
@Component
struct MainPage {
@State count: number = 0;
@State showDetail: boolean = true;
build() {
Column({ space: 20 }) {
// 全局用户名展示(单向)
UserName()
Divider()
// 计数器(本地状态)
Text(`计数:${this.count}`).fontSize(24)
Row({ space: 10 }) {
Button('+').onClick(() => { this.count++ })
Button('-').onClick(() => { this.count-- })
}
// 详情开关(通过 @Link 双向绑定)
DetailSwitch({ isOn: $showDetail })
// 根据 showDetail 显示详细信息
if (this.showDetail) {
Text('详细信息:当前计数器的值用于演示状态管理。')
.fontColor(Color.Gray)
}
}
.width('100%')
.padding(20)
}
}
@Component
struct UserName {
@StorageProp('username') username: string = '';
build() {
Text(`当前用户:${this.username}`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
}
}
@Component
struct DetailSwitch {
@Link isOn: boolean;
build() {
Row({ space: 10 }) {
Text('显示详情')
Toggle({ type: ToggleType.Switch, isOn: this.isOn })
.onChange((value: boolean) => {
this.isOn = value;
})
}
}
}
点击 +/- 只影响当前组件;切换开关会同步更新父组件;用户名从 AppStorage 读取,在其他页面修改 AppStorage 后此处会自动刷新。
七、常见错误与注意事项
7.1 直接修改 @State 对象的属性不触发更新
typescript
@State user: { name: string } = { name: 'Alice' };
this.user.name = 'Bob'; // ❌ UI 可能不刷新
解决方案 :整体替换对象 this.user = { ...this.user, name: 'Bob' }; 或使用 V2 状态管理。
7.2 @Prop 变量被当成双向绑定使用
typescript
// 错误预期:子组件修改 prop 能回传给父组件
@Prop value: string;
this.value = 'new'; // 仅本地副本变化,不影响父组件
请改用 @Link。
7.3 @Link 使用时忘记在父组件中使用 $ 传引用
typescript
// 错误
Child({ linkValue: this.parentValue }) // 被当作普通值传递,子组件 @Link 初始化失败
// 正确
Child({ linkValue: $parentValue })
7.4 AppStorage 未初始化就访问
在组件中绑定 @StorageLink 或 @StorageProp 前,确保调用过 AppStorage.setOrCreate 设置初始值,否则变量为 undefined 可能导致异常。
7.5 滥用 AppStorage
不要将 AppStorage 当作所有状态的垃圾桶。仅用于真正的全局共享状态,本地状态仍使用 @State 保持组件的独立性。
八、小结
| 概念 | 关键点 |
|---|---|
@State |
组件私有状态,赋值触发 UI 刷新 |
@Prop |
父传子单向数据流,子组件只读副本 |
@Link |
父传子双向数据流,通过 $ 传递引用 |
| AppStorage | 全局状态存储,跨页面/组件共享 |
@StorageProp |
从 AppStorage 单向读取 |
@StorageLink |
从 AppStorage 双向绑定 |
掌握这些状态管理基础后,你可以轻松处理大多数组件间数据通信的场景,为后续学习更复杂的页面逻辑和项目开发奠定坚实基础。
觉得文章有帮助?别忘了:
👍 点赞 👍 -- 给我一点鼓励
⭐ 收藏 ⭐ -- 方便以后查看
🔔 关注 🔔 -- 获取更新通知
标签: #HarmonyOS #ArkUI #状态管理 #@State #@Prop #@Link #AppStorage #学习笔记 #鸿蒙开发