前言

在鸿蒙 ArkTS 开发中,官方在 API 11 / HarmonyOS NEXT 阶段推出了全新的 V2 状态管理体系,对原有 V1 体系进行了系统性重构。两套体系在装饰器命名、响应式粒度、数据流方向等核心设计上均有显著变化。

本文从实际项目出发,系统梳理 V1 和 V2 的差异、各装饰器的使用方法,以及迁移时的注意事项。


一、为什么要有 V2?

V1 体系存在以下痛点:

  • 响应式粒度粗@State 标记整个对象,对象内任意属性变化都会触发整个组件重渲染,性能差
  • 数据流不透明@Link 双向绑定容易形成"数据迷宫",难以追踪变更来源
  • 嵌套对象不响应@Observed + @ObjectLink 才能处理嵌套对象,使用繁琐
  • 跨组件通信受限@Provide/@Consume@StorageLink 的能力边界模糊

V2 的目标是:精准响应、单向数据流、更清晰的组件契约


二、组件声明:@Component vs @ComponentV2

V1 写法

@Component
struct MyCard {
  @State count: number = 0

  build() {
    Text(`${this.count}`)
      .onClick(() => this.count++)
  }
}

V2 写法

@ComponentV2
struct MyCard {
  @Local count: number = 0

  build() {
    Text(`${this.count}`)
      .onClick(() => this.count++)
  }
}

关键区别:

  • @ComponentV2 是 V2 的入口,只能使用 V2 装饰器(@Local@Param 等)
  • @Component 只能使用 V1 装饰器(@State@Prop 等)
  • 两套体系不可混用,同一个 struct 内不能混合 V1/V2 装饰器

三、组件内部状态:@State vs @Local

V1 — @State

@Component
struct Counter {
  @State count: number = 0  // 整个变量被监听

  build() {
    Button(`count: ${this.count}`)
      .onClick(() => this.count++)
  }
}

V2 — @Local

@ComponentV2
struct Counter {
  @Local count: number = 0  // 组件内部私有状态

  build() {
    Button(`count: ${this.count}`)
      .onClick(() => this.count++)
  }
}

差异对比:

特性 @State(V1) @Local(V2)
是否可从外部传入 ✅ 可以(但不推荐) ❌ 严格私有,不接受外部传入
响应粒度 对象整体 对象整体(配合 @Trace 实现字段级)
语义 组件状态 组件内部私有状态(更明确)

四、父子传参:@Prop / @Link vs @Param / @Event

这是 V1 到 V2 变化最大的地方。

V1 — @Prop(单向)和 @Link(双向)

// 父组件
@Component
struct Parent {
  @State value: string = 'hello'

  build() {
    Column() {
      // @Prop 单向传入,子组件修改不影响父
      ChildA({ value: this.value })
      // @Link 双向绑定,子组件修改同步到父
      ChildB({ value: $this.value })
    }
  }
}

@Component
struct ChildA {
  @Prop value: string = ''  // 拷贝,修改不影响父
  build() { Text(this.value) }
}

@Component
struct ChildB {
  @Link value: string  // 引用,修改直接影响父
  build() {
    Text(this.value)
      .onClick(() => this.value = 'world')
  }
}

V2 — @Param(只读传入)和 @Event(事件回调)

V2 彻底移除了 @Link,统一为单向数据流 + 事件回调

// 父组件
@ComponentV2
struct Parent {
  @Local value: string = 'hello'

  build() {
    Child({
      value: this.value,
      onValueChange: (newVal: string) => {
        this.value = newVal  // 父组件自己决定是否更新
      }
    })
  }
}

// 子组件
@ComponentV2
struct Child {
  @Param value: string = ''          // 只读,不可直接修改
  @Event onValueChange: (v: string) => void = () => {}  // 事件回调

  build() {
    Text(this.value)
      .onClick(() => this.onValueChange('world'))  // 通过事件通知父组件
  }
}

差异对比:

特性 V1 V2
父→子传值 @Prop(拷贝) @Param(引用,只读)
子→父通信 @Link(双向绑定) @Event(单向事件)
数据流方向 双向(@Link) 强制单向
子组件能否直接修改父状态 ✅ 能(@Link) ❌ 不能,必须通过 @Event

@Param 使用要点:

  • 标注了 @Require 则调用方必须传入,否则编译报错
  • 子组件不得直接对 @Param 赋值,只能读取
  • 如需在子组件内修改,拷贝到 @Local 变量再操作

五、可观察对象:@Observed vs @ObservedV2 + @Trace

这是 V2 响应式最核心的改进:字段级精准响应

V1 — @Observed(对象级)

@Observed
class UserInfo {
  name: string = ''
  age: number = 0
  // name 或 age 任一变化,引用此对象的组件全部重渲染
}

@Component
struct UserCard {
  @ObjectLink user: UserInfo

  build() {
    Column() {
      Text(this.user.name)
      Text(`${this.user.age}`)
    }
  }
}

V1 的问题:修改 user.age 会导致显示 name 的 Text 也重渲染,即使 name 没变。

V2 — @ObservedV2 + @Trace(字段级)

@ObservedV2
class UserInfo {
  @Trace name: string = ''   // 只有这个字段变化才触发相关组件更新
  @Trace age: number = 0     // 独立追踪,互不影响
  role: string = 'user'      // 未标 @Trace,变化不触发更新
}

@ComponentV2
struct UserCard {
  @Param user: UserInfo = new UserInfo()

  build() {
    Column() {
      Text(this.user.name)      // 只在 name 变化时更新
      Text(`${this.user.age}`)  // 只在 age 变化时更新
    }
  }
}

差异对比:

特性 @Observed(V1) @ObservedV2 + @Trace(V2)
响应粒度 整个对象 单个字段(只有 @Trace 字段被追踪)
使用复杂度 需配合 @ObjectLink 直接 @Param 传入即可
嵌套对象 需每层都加 @Observed 嵌套类也加 @ObservedV2 + @Trace
性能 粗粒度,容易过度渲染 细粒度,精准更新

六、跨组件状态:@Provide/@Consume vs @Provider/@Consumer

V1 — @Provide / @Consume

@Component
struct GrandParent {
  @Provide('theme') theme: string = 'light'

  build() { Parent() }
}

@Component
struct Child {
  @Consume('theme') theme: string  // 跨层级直接消费,可直接修改

  build() {
    Text(this.theme)
      .onClick(() => this.theme = 'dark')  // 双向,影响所有使用者
  }
}

V2 — @Provider / @Consumer

@ComponentV2
struct GrandParent {
  @Provider() selectedIndex: number = 0  // 默认用变量名作 key

  build() { Parent() }
}

@ComponentV2
struct Child {
  @Consumer() selectedIndex: number = 0  // 读写,变化同步到 Provider

  build() {
    Text(`${this.selectedIndex}`)
      .onClick(() => this.selectedIndex = 1)
  }
}

差异对比:

特性 @Provide/@Consume(V1) @Provider/@Consumer(V2)
命名方式 字符串 key 默认变量名(可指定别名)
修改方向 双向(Consume 可直接改) 双向(Consumer 可直接改)
初始值 Consume 不设初始值 Consumer 必须设默认值(Provider 未找到时使用)

七、监听变化:@Watch vs @Monitor

V1 — @Watch

@Component
struct Page {
  @State @Watch('onCountChange') count: number = 0

  onCountChange() {
    console.log('count 变了:', this.count)
  }

  build() { /* ... */ }
}

V2 — @Monitor

@ComponentV2
struct Page {
  @Local count: number = 0
  @Local name: string = ''

  // 监听单个字段
  @Monitor('count')
  onCountChange(monitor: IMonitor) {
    const prev = monitor.value()?.before  // 变化前的值
    const curr = monitor.value()?.now     // 变化后的值
    console.log(`count: ${prev}${curr}`)
  }

  // 同时监听多个字段
  @Monitor('count', 'name')
  onAnyChange(monitor: IMonitor) {
    console.log('count 或 name 变化了')
  }

  build() { /* ... */ }
}

差异对比:

特性 @Watch(V1) @Monitor(V2)
绑定方式 标注在被监听字段上 独立方法,参数指定监听目标
获取变化前后值 ❌ 不支持 ✅ 通过 IMonitor 获取 before/now
监听多个字段 需要多个 @Watch 一个 @Monitor 可指定多个字段名
监听嵌套字段 ❌ 不支持 ✅ 支持路径(如 ‘user.name’)

八、计算属性:getter vs @Computed

V1 — 普通 getter

@Component
struct Page {
  @State firstName: string = '张'
  @State lastName: string = '三'

  // 普通 getter,每次访问都重新计算
  get fullName(): string {
    return this.firstName + this.lastName
  }

  build() {
    Text(this.fullName)
  }
}

V2 — @Computed

@ComponentV2
struct Page {
  @Local firstName: string = '张'
  @Local lastName: string = '三'

  // 自动缓存:只有依赖的字段变化时才重新计算
  @Computed
  get fullName(): string {
    return this.firstName + this.lastName
  }

  build() {
    Text(this.fullName)
  }
}

@Computed 的优势: 结果被缓存,依赖未变化时直接返回缓存值,避免重复计算。这在渲染列表或复杂 UI 时性能收益明显。


九、全局存储:AppStorage vs AppStorageV2

V1 — AppStorage

// 存入
AppStorage.setOrCreate('userInfo', new UserInfo())

// 组件中读取(双向绑定)
@Component
struct Page {
  @StorageLink('userInfo') userInfo: UserInfo = new UserInfo()
}

V2 — AppStorageV2

// 存入/获取(connect 模式,不存在则用工厂函数创建)
const userInfo = AppStorageV2.connect(UserInfo, () => new UserInfo())!

// 组件中读取
@ComponentV2
struct Page {
  userInfo: UserInfo = AppStorageV2.connect(UserInfo, () => new UserInfo())!

  build() {
    // userInfo 是 @ObservedV2 类,@Trace 字段自动响应
    Text(this.userInfo.nickname)
  }
}

差异对比:

特性 AppStorage(V1) AppStorageV2(V2)
Key 方式 字符串 类型本身(类名作 key)或自定义字符串
响应式 @StorageLink 双向绑定 依赖 @ObservedV2 + @Trace 响应
类型安全 弱类型(any) 强类型(泛型)

十、完整对照表

功能 V1 装饰器 V2 装饰器 说明
声明组件 @Component @ComponentV2 两套不可混用
内部状态 @State @Local V2 严格私有
父→子传值 @Prop @Param V2 为只读引用
必传参数 @Require @Param V2 可标注必传
子→父通信 @Link @Event V2 强制单向
双向绑定语法 $变量名 V2 不支持
可观察类 @Observed @ObservedV2
字段追踪 @Track(可选) @Trace(显式标注) V2 只追踪 @Trace 字段
嵌套引用 @ObjectLink @Param V2 更简洁
跨层共享 @Provide/@Consume @Provider/@Consumer
监听变化 @Watch @Monitor V2 支持获取变化前后值
计算属性 普通 getter @Computed V2 有缓存
全局存储 AppStorage + @StorageLink AppStorageV2.connect() V2 类型更安全

十一、迁移建议

1. 新项目直接用 V2

V2 是官方推荐的现代写法,新项目无需考虑 V1。

2. 老项目渐进迁移

V1 和 V2 组件可以在同一应用中共存(但不能在同一个 struct 内混用),可以按模块逐步替换。

3. 迁移检查清单

□ @Component        →  @ComponentV2
□ @State            →  @Local(内部状态)
□ @Prop             →  @Param(父传子,只读)
□ @Link             →  删除,改为 @Param + @Event 组合
□ @Observed         →  @ObservedV2
□ @Track / 无标注   →  @Trace(显式标注需要追踪的字段)
□ @ObjectLink       →  @Param(直接传 @ObservedV2 对象)
□ @Provide/@Consume →  @Provider/@Consumer
□ @Watch            →  @Monitor
□ 普通 getter       →  @Computed
□ AppStorage        →  AppStorageV2

4. 常见踩坑

踩坑一:@Param 不可赋值

// ❌ 错误:直接修改 @Param
@Param value: string = ''
this.value = 'new'  // 运行时报错

// ✅ 正确:通过 @Event 通知父组件修改
@Event onChange: (v: string) => void = () => {}
this.onChange('new')

踩坑二:@ObservedV2 类中忘记加 @Trace

@ObservedV2
class Config {
  theme: string = 'light'       // ❌ 没有 @Trace,修改不触发 UI 更新
  @Trace language: string = 'zh'  // ✅ 有 @Trace,修改触发更新
}

踩坑三:V1/V2 组件混用

@ComponentV2
struct MyComp {
  @State count: number = 0  // ❌ V2 组件内不能用 @State
  @Local count: number = 0  // ✅ 正确
}

十二、总结

V2 体系的核心设计理念是:明确的数据流方向 + 精准的响应粒度 + 清晰的组件契约

  • 单向数据流:通过删除 @Link、强制使用 @Event,让数据流向一目了然
  • 精准响应@ObservedV2 + @Trace 实现字段级响应,大幅减少不必要的重渲染
  • 类型安全AppStorageV2@Require @Param 等让组件接口更加明确

如果你正在开发新的鸿蒙应用,建议直接采用 V2 体系;如果是维护老项目,也可以按模块逐步迁移,两套体系可以在应用层面共存。


参考文档:HarmonyOS 官方文档 - 状态管理(V2)

本文基于 HarmonyOS NEXT(API 12,SDK 6.0)

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐