HarmonyOS 状态管理 V2 入门:@ComponentV2 与 V1→V2 装饰器全映射
前言
HarmonyOS 状态管理 V1(@Component + @State)在复杂业务中暴露出两个核心痛点:嵌套对象无法自动感知属性变化(必须借助 @Observed/@Track)、父子组件通信繁琐(@Prop/@Link/@Provide 语义混乱)。为此,HarmonyOS API 12 推出了状态管理 V2,以 @ComponentV2 为入口,引入一套更清晰、更强大的响应式装饰器体系。
本文作为 V2 系列的开篇,聚焦两件事:理解 V1→V2 的整体映射关系,以及掌握最基础的 @Local 私有状态用法。
核心 API
@ComponentV2 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 装饰器 | 职责 |
|---|---|---|
@State |
@Local |
组件私有状态 |
@Prop |
@Param |
父→子单向数据绑定 |
| 函数回调 | @Event |
子→父事件通知 |
@Provide |
@Provider |
跨层级状态提供方 |
@Consume |
@Consumer |
跨层级状态消费方 |
@Observed |
@ObservedV2 |
类级别深度追踪 |
@Track |
@Trace |
属性级别精细追踪 |
@Watch |
@Monitor |
状态变化监听回调 |
| 无 | @Computed |
派生状态缓存(新增) |
@Local:V2 组件私有状态
@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 替换 @Component;@Local 替换 @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 步:绑定状态到 UI
Text(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 12(HarmonyOS 5.0) 及以上。使用前确认module.json5中minAPIVersion >= 12。 - @Entry 与 @ComponentV2 可以共存:入口页面同时加
@Entry和@ComponentV2是合法写法,等价于 V1 的@Entry @Component。
更多推荐



所有评论(0)