前言
在鸿蒙 ArkTS 开发中,官方在 API 11 / HarmonyOS NEXT 阶段推出了全新的 V2 状态管理体系,对原有 V1 体系进行了系统性重构。两套体系在装饰器命名、响应式粒度、数据流方向等核心设计上均有显著变化。
本文从实际项目出发,系统梳理 V1 和 V2 的差异、各装饰器的使用方法,以及迁移时的注意事项。
一、为什么要有 V2?
V1 体系存在以下痛点:
- 响应式粒度粗:
@State 标记整个对象,对象内任意属性变化都会触发整个组件重渲染,性能差 - 数据流不透明:
@Link 双向绑定容易形成"数据迷宫",难以追踪变更来源 - 嵌套对象不响应:
@Observed + @ObjectLink 才能处理嵌套对象,使用繁琐 - 跨组件通信受限:
@Provide/@Consume 与 @StorageLink 的能力边界模糊
V2 的目标是:精准响应、单向数据流、更清晰的组件契约。
二、组件声明:@Component vs @ComponentV2
V1 写法
@Componentstruct MyCard { @State 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
@Componentstruct Counter { @State count: number = 0 // 整个变量被监听 build() { Button(`count: {this.count}`) .onClick(() => this.count++) }}
差异对比:
四、父子传参:@Prop / @Link vs @Param / @Event
这是 V1 到 V2 变化最大的地方。
V1 — @Prop(单向)和 @Link(双向)
// 父组件@Componentstruct Parent { @State value: string = 'hello' build() { Column() { // @Prop 单向传入,子组件修改不影响父 ChildA({ value: this.value }) // @Link 双向绑定,子组件修改同步到父 ChildB({ value: {this.user.age}`) } }}
V1 的问题:修改 user.age 会导致显示 name 的 Text 也重渲染,即使 name 没变。
V2 — @ObservedV2 + @Trace(字段级)
@ObservedV2class UserInfo { @Trace name: string = '' // 只有这个字段变化才触发相关组件更新 @Trace age: number = 0 // 独立追踪,互不影响 role: string = 'user' // 未标 @Trace,变化不触发更新}@ComponentV2struct UserCard { @Param user: UserInfo = new UserInfo() build() { Column() { Text(this.user.name) // 只在 name 变化时更新 Text(`{this.selectedIndex}`) .onClick(() => this.selectedIndex = 1) }}
差异对比:
| | |
|---|
| | |
| | |
| | Consumer 必须设默认值(Provider 未找到时使用) |
七、监听变化:@Watch vs @Monitor
V1 — @Watch
@Componentstruct Page { @State @Watch('onCountChange') count: number = 0 onCountChange() { console.log('count 变了:', this.count) } build() { /* ... */ }}
V2 — @Monitor
@ComponentV2struct 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: {curr}`) } // 同时监听多个字段 @Monitor('count', 'name') onAnyChange(monitor: IMonitor) { console.log('count 或 name 变化了') } build() { /* ... */ }}
差异对比:
| | |
|---|
| | |
| | ✅ 通过 IMonitor 获取 before/now |
| | |
| | |
八、计算属性:getter vs @Computed
V1 — 普通 getter
@Componentstruct Page { @State firstName: string = '张' @State lastName: string = '三' // 普通 getter,每次访问都重新计算 get fullName(): string { return this.firstName + this.lastName } build() { Text(this.fullName) }}
V2 — @Computed
@ComponentV2struct 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())// 组件中读取(双向绑定)@Componentstruct Page { @StorageLink('userInfo') userInfo: UserInfo = new UserInfo()}
V2 — AppStorageV2
// 存入/获取(connect 模式,不存在则用工厂函数创建)const userInfo = AppStorageV2.connect(UserInfo, () => new UserInfo())!// 组件中读取@ComponentV2struct Page { userInfo: UserInfo = AppStorageV2.connect(UserInfo, () => new UserInfo())! build() { // userInfo 是 @ObservedV2 类,@Trace 字段自动响应 Text(this.userInfo.nickname) }}
差异对比:
| | |
|---|
| | |
| | 依赖 @ObservedV2 + @Trace 响应 |
| | |
十、完整对照表
| | | |
|---|
| @Component | @ComponentV2 | |
| @State | @Local | |
| @Prop | @Param | |
| | @Require @Param | |
| @Link | @Event | |
| $变量名 | | |
| @Observed | @ObservedV2 | |
| @Track | @Trace | |
| @ObjectLink | @Param | |
| @Provide/@Consume | @Provider/@Consumer | |
| @Watch | @Monitor | |
| | @Computed | |
| AppStorage | AppStorageV2.connect() | |
十一、迁移建议
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
@ObservedV2class Config { theme: string = 'light' // ❌ 没有 @Trace,修改不触发 UI 更新 @Trace language: string = 'zh' // ✅ 有 @Trace,修改触发更新}
踩坑三:V1/V2 组件混用
@ComponentV2struct MyComp { @State count: number = 0 // ❌ V2 组件内不能用 @State @Local count: number = 0 // ✅ 正确}
十二、总结
V2 体系的核心设计理念是:明确的数据流方向 + 精准的响应粒度 + 清晰的组件契约。
- 单向数据流:通过删除
@Link、强制使用 @Event,让数据流向一目了然 - 精准响应:
@ObservedV2 + @Trace 实现字段级响应,大幅减少不必要的重渲染 - 类型安全:
AppStorageV2、@Require @Param 等让组件接口更加明确
如果你正在开发新的鸿蒙应用,建议直接采用 V2 体系;如果是维护老项目,也可以按模块逐步迁移,两套体系可以在应用层面共存。