HarmonyOS 状态管理 V2 入门:@ComponentV2 与 V1→V2 装饰器全映射
前言HarmonyOS 状态管理 V1ComponentState在复杂业务中暴露出两个核心痛点嵌套对象无法自动感知属性变化必须借助Observed/Track、父子组件通信繁琐Prop/Link/Provide语义混乱。为此HarmonyOS API 12 推出了状态管理 V2以ComponentV2为入口引入一套更清晰、更强大的响应式装饰器体系。本文作为 V2 系列的开篇聚焦两件事理解 V1→V2 的整体映射关系以及掌握最基础的Local私有状态用法。核心 APIComponentV2 vs Component// V1 写法 Entry Component struct MyPage { State count: number 0 build() { ... } } // V2 写法 Entry ComponentV2 struct MyPage { Local count: number 0 build() { ... } }ComponentV2是 V2 状态管理的入口装饰器替换Component。两者不能混用——V2 组件内只能使用 V2 装饰器Local/Param/Event等V1 装饰器State/Prop等在ComponentV2组件中无效。V1 → V2 装饰器全映射V1 装饰器V2 装饰器职责StateLocal组件私有状态PropParam父→子单向数据绑定函数回调Event子→父事件通知ProvideProvider跨层级状态提供方ConsumeConsumer跨层级状态消费方ObservedObservedV2类级别深度追踪TrackTrace属性级别精细追踪WatchMonitor状态变化监听回调无Computed派生状态缓存新增LocalV2 组件私有状态Local count: number 0 // 基本类型 Local profile: Profile { ... } // 对象类型替换整体触发更新 Local items: string[] [] // 数组类型Local等价于 V1 的State区别在于深度观测配合ObservedV2/Trace可感知类属性级别的变化V1 需要Observed/Track更严格的私有性Local标记的状态不能被父组件直接读取或修改整体替换触发刷新对于普通对象/接口用不可变方式替换整体对象即可触发 UI 更新实现思路用Local管理一个Profile接口对象包含name/score/level三个字段。每次点击按钮时用不可变方式整体替换this.profile而非修改属性让Local检测到引用变化并触发重渲染。同时维护Local logs: string[]记录每次操作验证Local对数组的响应能力。对比表以private readonly数组存储避免 ArkTS 严格模式下内联对象字面量推断失败。逐步实现第 1 步声明 ComponentV2 和 Local 状态interface Profile { name: string score: number level: string } Entry ComponentV2 struct Index { Local profile: Profile { name: 鸿蒙开发者, score: 0, level: 初级 } Local clickCount: number 0 Local logs: string[] []ComponentV2替换ComponentLocal替换State。Profile使用interface不是类因为我们用不可变替换触发更新不需要ObservedV2。第 2 步用不可变方式更新对象private addScore(delta: number): void { this.clickCount const newScore Math.max(0, this.profile.score delta) const newLevel this.levelOf(newScore) // 整体替换触发 Local 响应式更新 this.profile { name: this.profile.name, score: newScore, level: newLevel } this.logs [第${this.clickCount}次${delta 0 ? : }${delta} 分 → ${newScore} 分, ...this.logs.slice(0, 4)] }this.profile { ... }产生新引用Local检测到引用变化后重渲染。this.logs [新元素, ...旧数组]同理——数组整体替换触发更新。第 3 步绑定状态到 UIText(this.profile.level) .backgroundColor(this.levelColor(this.profile.level)) // 等级变化 → 颜色自动更新 Progress({ value: Math.min(this.profile.score, 100), total: 100 }) .color(#0066ff) // 分数变化 → 进度条自动更新Local变量的所有读取点都自动注册为依赖状态变化时精确刷新相关节点。第 4 步ArkTS 严格模式下的对象数组声明ArkTS 禁止内联匿名类型的对象字面量数组arkts-no-untyped-obj-literals对照表需先声明接口再用属性存储interface CompareRow { v1: string; v2: string; desc: string } // 声明为 private readonly不触发响应式节省开销 private readonly compareRows: CompareRow[] [ { v1: State, v2: Local, desc: 组件私有状态 }, ... ] // ForEach 回调参数类型明确为接口 ForEach(this.compareRows, (item: CompareRow) { ... })完整代码// Article 61: ComponentV2 状态管理 V2 入门 interface Profile { name: string score: number level: string } interface CompareRow { v1: string v2: string desc: string } Entry ComponentV2 struct Index { Local profile: Profile { name: 鸿蒙开发者, score: 0, level: 初级 } Local clickCount: number 0 Local logs: string[] [] private readonly compareRows: CompareRow[] [ { v1: Component, v2: ComponentV2, desc: 组件声明 }, { v1: State, v2: Local, desc: 组件私有状态 }, { v1: Prop, v2: Param, desc: 父→子单向绑定 }, { v1: 函数回调, v2: Event, desc: 子→父通知 }, { v1: Provide, v2: Provider, desc: 跨层级提供 }, { v1: Consume, v2: Consumer, desc: 跨层级消费 }, { v1: Observed, v2: ObservedV2, desc: 类级别深度追踪 }, { v1: Track, v2: Trace, desc: 属性级别追踪 }, { v1: Watch, v2: Monitor, desc: 状态变化监听 }, { v1: 无, v2: Computed, desc: 派生状态缓存 }, ] private levelOf(score: number): string { if (score 100) return 高级 if (score 50) return 中级 return 初级 } private levelColor(level: string): string { if (level 高级) return #ff6b35 if (level 中级) return #0066ff return #07c160 } private addScore(delta: number): void { this.clickCount const newScore Math.max(0, this.profile.score delta) const newLevel this.levelOf(newScore) this.profile { name: this.profile.name, score: newScore, level: newLevel } const prefix delta 0 ? ${delta} : ${delta} this.logs [第${this.clickCount}次${prefix} 分 → ${newScore} 分${newLevel}, ...this.logs.slice(0, 4)] } build() { Column({ space: 0 }) { Row() { Text(ComponentV2 状态管理 V2).fontSize(18).fontWeight(FontWeight.Bold).fontColor(#1a1a1a) } .width(100%).height(56).backgroundColor(#ffffff).padding({ left: 16 }) .border({ width: { bottom: 1 }, color: #f0f0f0 }) Scroll() { Column({ space: 12 }) { Column({ space: 12 }) { Text(开发者档案).fontSize(13).fontColor(#888).fontWeight(FontWeight.Medium).width(100%) Row({ space: 14 }) { Text().fontSize(44) Column({ space: 6 }) { Text(this.profile.name) .fontSize(18).fontWeight(FontWeight.Bold).fontColor(#1a1a1a) Row({ space: 8 }) { Text(this.profile.level) .fontSize(12).fontColor(#fff).padding({ left: 10, right: 10, top: 3, bottom: 3 }) .backgroundColor(this.levelColor(this.profile.level)).borderRadius(10) Text(${this.profile.score} 分).fontSize(14).fontColor(#555) } } .alignItems(HorizontalAlign.Start).layoutWeight(1) } .width(100%) Column({ space: 4 }) { Row() { Text(升级进度).fontSize(11).fontColor(#aaa).layoutWeight(1) Text(${Math.min(this.profile.score, 100)}/100).fontSize(11).fontColor(#aaa) } .width(100%) Progress({ value: Math.min(this.profile.score, 100), total: 100 }) .color(#0066ff).backgroundColor(#eee).width(100%).height(6).borderRadius(3) } } .width(100%).backgroundColor(#ffffff).borderRadius(12).padding(16) .margin({ left: 12, right: 12 }) Column({ space: 10 }) { Text(修改嵌套对象属性).fontSize(13).fontColor(#888).fontWeight(FontWeight.Medium).width(100%) Row({ space: 10 }) { Button(-20 分) .layoutWeight(1).height(44).borderRadius(22).fontSize(14) .backgroundColor(#fff3f3).fontColor(#e53935) .onClick(() this.addScore(-20)) Button(10 分) .layoutWeight(1).height(44).borderRadius(22).fontSize(14) .backgroundColor(#e8f5e9).fontColor(#2e7d32) .onClick(() this.addScore(10)) Button(30 分) .layoutWeight(1).height(44).borderRadius(22).fontSize(14) .backgroundColor(#e3f2fd).fontColor(#1565c0) .onClick(() this.addScore(30)) } Text(点击后 Local 检测到对象替换自动刷新 UI) .fontSize(11).fontColor(#bbb).width(100%) } .width(100%).backgroundColor(#ffffff).borderRadius(12).padding(16) .margin({ left: 12, right: 12 }) Column({ space: 4 }) { Text(V1 vs V2 装饰器对照).fontSize(13).fontColor(#888).fontWeight(FontWeight.Medium).width(100%) ForEach(this.compareRows, (item: CompareRow) { Row() { Text(item.v1).fontSize(11).fontColor(#999).fontFamily(monospace).width(100) Text(→).fontSize(11).fontColor(#ccc).margin({ left: 4, right: 4 }) Text(item.v2).fontSize(11).fontColor(#0066ff).fontFamily(monospace) .fontWeight(FontWeight.Bold).width(110) Text(item.desc).fontSize(11).fontColor(#888).layoutWeight(1) } .width(100%).padding({ top: 7, bottom: 7 }) .border({ width: { bottom: 1 }, color: #f5f5f5 }) }) } .width(100%).backgroundColor(#ffffff).borderRadius(12).padding(16) .margin({ left: 12, right: 12 }) Column({ space: 6 }) { Text(操作日志).fontSize(13).fontColor(#888).fontWeight(FontWeight.Medium).width(100%) if (this.logs.length 0) { Text(点击上方按钮查看 Local 驱动 UI 更新的效果) .fontSize(12).fontColor(#ccc).width(100%) } else { ForEach(this.logs, (log: string, idx: number) { Text(log) .fontSize(13).fontColor(idx 0 ? #0066ff : #999) .width(100%).lineHeight(22) }) } } .width(100%).backgroundColor(#ffffff).borderRadius(12).padding(16) .margin({ left: 12, right: 12, bottom: 24 }) } .width(100%).padding({ top: 12 }) } .layoutWeight(1).backgroundColor(#f8f8f8) } .width(100%).height(100%).backgroundColor(#f8f8f8) } }运行效果初始态初级0 分进度条空Local 更新后高级120 分进度条满格等级徽标变橙色注意事项V1 与 V2 不能混用ComponentV2组件内不能使用State/Prop/Link等 V1 装饰器反之亦然。嵌套时也不允许 V2 组件作为 V1 组件的子组件或反过来需要统一迁移。Local 的对象更新方式对于普通类/接口对象直接修改属性this.profile.score不会触发重渲染必须整体替换this.profile { ...this.profile, score: newScore }或配合ObservedV2/Trace实现属性级追踪见 article66。ArkTS 严格模式下的数组字面量在 ForEach 中传入含对象字面量的内联数组会触发arkts-no-untyped-obj-literals错误正确做法是先声明接口再用组件属性private readonly存储数组ForEach 回调参数明确标注接口类型。API 版本要求ComponentV2及配套装饰器要求API Level 12HarmonyOS 5.0及以上。使用前确认module.json5中minAPIVersion 12。Entry 与 ComponentV2 可以共存入口页面同时加Entry和ComponentV2是合法写法等价于 V1 的Entry Component。