鸿蒙状态管理
What — 是什么
鸿蒙状态管理是 ArkUI 声明式框架的核心机制,通过装饰器体系管理组件间数据的流动和 UI 刷新。ArkUI 采用”状态驱动 UI”的模式,状态变量的变化自动触发依赖该状态的组件重新渲染,开发者只需声明状态与 UI 的映射关系。
核心概念:
- @State:组件内状态,值变化触发当前组件 rebuild
- @Prop:父到子单向同步,子组件可修改但不影响父
- @Link:父子双向同步,子组件修改直接反映到父
- @Provide/@Consume:跨层级数据传递,类似 React Context
- @Observed/@ObjectLink:嵌套对象深度观测
- @Watch:状态变更监听回调
- AppStorage:应用级全局状态
- LocalStorage:页面/Ability 级状态
- PersistentStorage:持久化全局状态
装饰器体系:
┌──────────────────────────────────────────────────┐
│ 鸿蒙状态管理装饰器 │
├──────────────┬───────────────────────────────────┤
│ 组件内 │ @State │
│ │ @Watch │
├──────────────┼───────────────────────────────────┤
│ 父子通信 │ @Prop (单向) / @Link (双向) │
├──────────────┼───────────────────────────────────┤
│ 跨层级通信 │ @Provide / @Consume │
├──────────────┼───────────────────────────────────┤
│ 嵌套对象 │ @Observed / @ObjectLink │
├──────────────┼───────────────────────────────────┤
│ V2 装饰器 │ @ObservedV2 / @Trace │
├──────────────┼───────────────────────────────────┤
│ 全局状态 │ AppStorage / LocalStorage │
│ │ PersistentStorage │
└──────────────┴───────────────────────────────────┘
Why — 为什么
适用场景:
- 组件内部状态管理(@State)
- 父子组件数据传递(@Prop/@Link)
- 跨层级数据共享(@Provide/@Consume)
- 全局应用状态(AppStorage)
- 状态持久化(PersistentStorage)
对比其他框架:
| 维度 | 鸿蒙 | Flutter | Vue 3 | React |
|---|---|---|---|---|
| 组件内状态 | @State | State | ref/reactive | useState |
| 父子通信 | @Prop/@Link | 构造参数/回调 | props/emit | props/callback |
| 跨层级 | @Provide/@Consume | InheritedWidget | provide/inject | Context |
| 全局状态 | AppStorage | Provider/Riverpod | Pinia | Redux/Zustand |
| 双向绑定 | @Link | 不支持 | v-model | 不支持 |
| 持久化 | PersistentStorage | SharedPreferences | localStorage | localStorage |
How — 怎么用
1. @State 组件内状态
@Component
struct CounterPage {
@State count: number = 0
@State items: string[] = []
@State user: User = new User('Alice', 25)
build() {
Column() {
// 基本类型:值变化触发 rebuild
Text(`Count: ${this.count}`)
.fontSize(24)
Button('Increment')
.onClick(() => this.count++)
// 数组:增删元素触发 rebuild
ForEach(this.items, (item: string) => {
Text(item)
})
Button('Add Item')
.onClick(() => this.items.push(`Item ${this.items.length + 1}`))
// 对象:赋值新对象触发 rebuild
Text(`User: ${this.user.name}, ${this.user.age}`)
Button('Change Name')
.onClick(() => {
this.user = new User('Bob', 30) // 整体赋值才触发
})
}
}
}
// ⚠️ @State 对象类型的观察机制
// 基本类型:值变化即触发
// 数组:push/pop/shift/unshift/splice 触发
// 对象:整体赋值触发,属性修改不触发(需 @Observed)
2. @Prop 父到子单向同步
// 父组件
@Component
struct ParentComponent {
@State title: string = 'Hello'
@State count: number = 0
build() {
Column() {
Text(`Parent count: ${this.count}`)
// @Prop 单向:父组件变化同步到子,子组件修改不影响父
ChildComponent({ title: this.title, count: this.count })
}
}
}
// 子组件
@Component
struct ChildComponent {
@Prop title: string = '' // 父→子单向
@Prop count: number = 0 // 父→子单向
build() {
Column() {
Text(this.title)
Text(`Child count: ${this.count}`)
// @Prop 修改只影响自身,不同步回父
Button('Increment in Child')
.onClick(() => this.count++) // 不会影响 ParentComponent 的 count
}
}
}
3. @Link 父子双向同步
// 父组件
@Component
struct ParentComponent {
@State username: string = 'Alice'
@State score: number = 0
build() {
Column() {
Text(`Parent: ${this.username}, Score: ${this.score}`)
// @Link 双向:子组件修改直接反映到父
// 传递时用 $ 前缀表示双向绑定
EditComponent({ username: $username, score: $score })
}
}
}
// 子组件
@Component
struct EditComponent {
@Link username: string // 双向同步
@Link score: number // 双向同步
build() {
Column() {
TextInput({ text: this.username })
.onChange((value) => {
this.username = value // 直接修改,同步到父
})
Button('Add Score')
.onClick(() => this.score++) // 直接修改,同步到父
}
}
}
4. @Provide/@Consume 跨层级传递
// 上层组件提供数据
@Component
struct GrandParentComponent {
@Provide theme: string = 'light'
@Provide user: User = new User('Alice', 25)
build() {
Column() {
Text(`Theme: ${this.theme}`)
Button('Toggle Theme')
.onClick(() => this.theme = this.theme === 'light' ? 'dark' : 'light')
ParentComponent()
}
}
}
// 中间组件不需要转发
@Component
struct ParentComponent {
build() {
Column() {
ChildComponent() // 不需要传递 theme/user
}
}
}
// 任意后代组件消费数据
@Component
struct ChildComponent {
@Consume theme: string // 自动匹配 @Provide 的同名变量
@Consume user: User
build() {
Column() {
Text(`Current theme: ${this.theme}`)
Text(`User: ${this.user.name}`)
Button('Switch')
.onClick(() => this.theme = 'dark') // 修改会同步到 @Provide
}
}
}
5. @Observed/@ObjectLink 嵌套对象深度观测
// @State 对对象属性变化不敏感(只观察整体赋值)
// @Observed + @ObjectLink 可以深度观测对象属性变化
@Observed
class User {
name: string = ''
age: number = 0
address: Address = new Address()
constructor(name: string, age: number) {
this.name = name
this.age = age
}
}
@Observed
class Address {
city: string = ''
street: string = ''
}
// 父组件
@Component
struct UserPage {
@State user: User = new User('Alice', 25)
build() {
Column() {
// 直接修改属性 → 需要 @ObjectLink 才能触发子组件刷新
UserInfo({ user: this.user })
Button('Change Name')
.onClick(() => {
this.user.name = 'Bob' // @Observed 对象属性变化会通知
})
}
}
}
// 子组件用 @ObjectLink 接收 @Observed 对象
@Component
struct UserInfo {
@ObjectLink user: User // 接收 @Observed 对象
build() {
Column() {
Text(this.user.name) // 属性变化会触发刷新
Text(`${this.user.age}`)
}
}
}
6. @Watch 状态监听
@Component
struct WatchDemo {
@State @Watch('onCountChanged') count: number = 0
@State @Watch('onNameChanged') name: string = ''
// 监听 count 变化
onCountChanged(oldValue: number, newValue: number) {
console.log(`Count: ${oldValue} → ${newValue}`)
if (newValue >= 10) {
console.log('Reached limit!')
}
}
// 监听 name 变化
onNameChanged(oldValue: string, newValue: string) {
console.log(`Name: ${oldValue} → ${newValue}`)
}
build() {
Column() {
Text(`Count: ${this.count}`)
Text(`Name: ${this.name}`)
Button('Increment').onClick(() => this.count++)
TextInput({ text: this.name }).onChange((v) => this.name = v)
}
}
}
7. V2 装饰器(@ObservedV2/@Trace)
// V2 装饰器提供更精细的深度观测能力
@ObservedV2
class UserModel {
@Trace name: string = ''
@Trace age: number = 0
@Trace address: AddressModel = new AddressModel()
constructor(name: string, age: number) {
this.name = name
this.age = age
}
}
@ObservedV2
class AddressModel {
@Trace city: string = ''
@Trace street: string = ''
}
// 使用时和 V1 类似,但观测粒度更细
// @Trace 标记的属性变化会精确触发依赖该属性的组件更新
// 比 @Observed 性能更好,只刷新真正变化的部分
8. AppStorage 全局状态
// ===== 初始化全局状态 =====
// 在 EntryAbility.ets 中
AppStorage.setOrCreate('token', '')
AppStorage.setOrCreate('isLogin', false)
AppStorage.setOrCreate('userInfo', new UserInfo())
// ===== 组件中使用 =====
@Component
struct ProfilePage {
@StorageLink('token') token: string = '' // 双向绑定
@StorageProp('isLogin') isLogin: boolean = false // 单向绑定
build() {
Column() {
if (this.isLogin) {
Text(`Token: ${this.token}`)
Button('Logout').onClick(() => {
this.token = ''
AppStorage.setOrCreate('isLogin', false)
})
} else {
Button('Login').onClick(() => {
this.token = 'new_token'
AppStorage.setOrCreate('isLogin', true)
})
}
}
}
}
// ===== 在非 UI 代码中访问 =====
// 读取
const token = AppStorage.get<string>('token')
// 写入
AppStorage.setOrCreate('token', 'new_value')
9. LocalStorage 页面级状态
// LocalStorage 作用域为 Ability 或页面
// 在 UIAbility 中创建
export default class EntryAbility extends UIAbility {
storage: LocalStorage = new LocalStorage()
onWindowStageCreate(windowStage: window.WindowStage) {
this.storage.setOrCreate('pageCount', 0)
windowStage.loadContent('pages/Index', this.storage)
}
}
// 页面中使用
@Component
struct PageWithLocal {
@LocalStorageLink('pageCount') count: number = 0
build() {
Column() {
Text(`Page Count: ${this.count}`)
Button('Add').onClick(() => this.count++)
}
}
}
10. PersistentStorage 持久化
// ===== 持久化全局状态(写入文件系统)=====
// 在 EntryAbility.ets 中初始化
PersistentStorage.persistProp('isFirstLaunch', true)
PersistentStorage.persistProp('themeMode', 'light')
// 使用(和 AppStorage 一样,但会持久化)
@Component
struct SettingsPage {
@StorageLink('isFirstLaunch') isFirstLaunch: boolean = true
@StorageLink('themeMode') themeMode: string = 'light'
build() {
Column() {
Text(`First Launch: ${this.isFirstLaunch}`)
Text(`Theme: ${this.themeMode}`)
Toggle({ type: ToggleType.Switch, isOn: this.themeMode === 'dark' })
.onChange((isOn) => {
this.themeMode = isOn ? 'dark' : 'light' // 自动持久化
})
Button('Mark Launched')
.onClick(() => this.isFirstLaunch = false) // 自动持久化
}
}
}
// 注意:
// 1. PersistentStorage 只支持简单类型(number/string/boolean)
// 2. 不支持复杂对象
// 3. 适合少量配置项,大量数据用 RDB 数据库
常见问题与踩坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 对象属性变化 UI 不更新 | @State 不深度观测 | 使用 @Observed/@ObjectLink |
| @Link 传值报错 | 未用 $ 前缀 | 传递时用 $变量名 |
| @Provide/@Consume 匹配失败 | 变量名不一致 | 确保同名或用 alias |
| AppStorage 类型错误 | 初始值类型不匹配 | setOrCreate 时指定正确类型 |
| @Watch 死循环 | 回调中又修改自身 | 加条件判断避免循环 |
| PersistentStorage 不持久 | 只支持简单类型 | 复杂对象用 JSON.stringify |
| @Prop 修改不同步父 | @Prop 是单向的 | 用 @Link 实现双向同步 |
最佳实践
- 组件内状态优先用 @State
- 父子通信:只读用 @Prop,需同步回父用 @Link
- 跨层级用 @Provide/@Consume,避免逐层传递
- 嵌套对象用 @Observed/@ObjectLink 或 V2 的 @ObservedV2/@Trace
- 全局状态用 AppStorage + @StorageLink
- 配置持久化用 PersistentStorage
- @Watch 避免循环修改,加条件判断
- @Trace 标记最小粒度属性,优化刷新性能
面试题
Q1: @State、@Prop、@Link 三者有什么区别?
@State 是组件内状态,值变化触发当前组件 rebuild。@Prop 是父到子单向同步,父组件状态变化会同步到子组件,但子组件修改不影响父(单向数据流)。@Link 是父子双向同步,子组件修改直接反映到父组件,传递时需要用 $ 前缀(如 username: $username)。选择原则:组件内部管理用 @State;只读展示用 @Prop;需要子组件修改并同步回父用 @Link。
Q2: @Observed/@ObjectLink 解决了什么问题?和 @State 有什么区别?
@State 对对象类型的观测只到第一层:整体赋值(this.user = newUser)触发刷新,但修改属性(this.user.name = ‘Bob’)不触发。@Observed + @ObjectLink 实现了深度观测:@Observed 标记的类,其属性变化会被框架追踪;@ObjectLink 在子组件中接收 @Observed 对象,属性变化精确触发子组件刷新。区别:@State 是值观测(整体赋值),@ObjectLink 是引用观测(属性级追踪)。适用场景:对象属性需要频繁修改并触发 UI 更新时使用。
Q3: @Provide/@Consume 和 @Prop/@Link 有什么区别?分别适合什么场景?
@Prop/@Link 是父子直连通信,数据必须逐层传递(A→B→C→D),中间层需要声明转发。@Provide/@Consume 是跨层级通信,上层组件 @Provide 提供数据,任意后代组件 @Consume 消费数据,中间层无需感知。@Provide/@Consume 通过变量名(或 alias)自动匹配。选择:父子直接通信用 @Prop/@Link(明确数据来源,可追踪);跨多层传递用 @Provide/@Consume(避免逐层转发,代码简洁)。注意 @Provide/@Consume 是按名称匹配的,多层级同名可能导致意外匹配。
Q4: AppStorage 和 LocalStorage 有什么区别?
AppStorage 是应用级全局状态,整个应用共享一份,跨 Ability 跨页面都可访问。LocalStorage 是页面/Ability 级状态,作用域限于创建它的 Ability 或通过 loadContent 传入的页面树。AppStorage 适合应用全局数据(登录态、用户信息、主题),LocalStorage 适合页面级数据(当前页面的临时状态)。绑定方式:AppStorage 用 @StorageLink/@StorageProp,LocalStorage 用 @LocalStorageLink/@LocalStorageProp。全局共享用 AppStorage,页面隔离用 LocalStorage。
Q5: PersistentStorage 是如何实现持久化的?有什么限制?
PersistentStorage 将键值对持久化到文件系统,应用重启后自动恢复。初始化时用 persistProp/persistProps 声明需要持久化的属性,之后通过 AppStorage 的 @StorageLink 读写,框架自动同步到磁盘。限制:①只支持简单类型(number/string/boolean/Array/Object of simple types);②不支持复杂自定义对象(需 JSON.stringify 序列化);③读写有 I/O 开销,不适合高频更新;④存储容量有限(建议不超过 4KB);⑤不适合大量数据,大数据用 RDB 数据库。适合场景:用户配置、登录令牌、首次启动标识等少量配置数据。
Q6: @Watch 的执行时机是什么?如何避免 @Watch 导致的死循环?
@Watch 在状态变量变化后、组件 build 之前执行,回调接收 oldValue 和 newValue 两个参数。避免死循环:①在回调中不要无条件修改自身监听的变量;②加条件判断,只有真正需要变化时才修改(如 if (newValue !== targetValue) this.variable = targetValue);③避免多个 @Watch 互相触发形成环。@Watch 适合的副作用:数据变化时触发网络请求、日志记录、计算派生值,不适合作为常规的状态修改方式。
Q7: V2 装饰器(@ObservedV2/@Trace)和 V1 有什么改进?
V2 的核心改进是更精细的观测粒度和更好的性能。V1 的 @Observed 对整个对象做代理,任何属性变化都触发通知;V2 的 @Trace 只标记需要观测的属性,未标记的属性变化不触发通知,减少不必要的刷新。V2 优势:①按需观测,未 @Trace 的属性变化不触发刷新;②性能更好,观测开销更小;③与 V1 可以共存,逐步迁移。V2 使用方式:@ObservedV2 标记类,@Trace 标记需要观测的属性。新项目推荐使用 V2。
Q8: 鸿蒙状态管理和 Flutter 的 Provider/Riverpod 有什么核心区别?
核心区别:①鸿蒙的状态管理是语言级内建的(装饰器),Flutter 需要第三方库。②鸿蒙原生支持双向绑定(@Link),Flutter 原生只有单向数据流。③鸿蒙的 @Provide/@Consume 是编译时绑定的,Flutter 的 InheritedWidget/Provider 是运行时查找的。④鸿蒙的 AppStorage 是全局单例,Flutter 的 Riverpod 是依赖注入。⑤鸿蒙的 @Watch 是声明式副作用,Flutter 没有等价机制(需要 ref.listen)。总体上鸿蒙的状态管理更内聚,Flutter 更灵活但需要选型。
相关链接: