鸿蒙状态管理

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)

对比其他框架:

维度鸿蒙FlutterVue 3React
组件内状态@StateStateref/reactiveuseState
父子通信@Prop/@Link构造参数/回调props/emitprops/callback
跨层级@Provide/@ConsumeInheritedWidgetprovide/injectContext
全局状态AppStorageProvider/RiverpodPiniaRedux/Zustand
双向绑定@Link不支持v-model不支持
持久化PersistentStorageSharedPreferenceslocalStoragelocalStorage

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
    }
  }
}
// 父组件
@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
    }
  }
}
// @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 更灵活但需要选型。


相关链接: