从 状态管理 V1 到 V2:鸿蒙开发者的进化指南
本文对比了状态管理V1(@Component)和V2(@ComponentV2)的核心差异。V2引入了深度观察机制,通过@ObservedV2和@Trace装饰器实现细粒度更新,解决了V1中嵌套对象修改不触发刷新的痛点。主要变化包括:装饰器语法简化(如@Param替代@Prop/@Link)、新增@Computed计算属性、优化生命周期(新增onDidBuild)、强制单向数据流设计。V2虽然牺牲
如果你已经习惯了 状态管理 V1 (@Component) 的开发模式,切换到 V2 (@ComponentV2) 可能会让你感到既熟悉又陌生。V2 不仅仅是简单的语法糖升级,它在状态管理和渲染性能上有着本质的改变。
这篇文章将带你快速掌握核心区别,助你无缝迁移。

1. 核心思维转变:从“代理”到“深度观察”
- V1 的痛点:V1 的状态观察通常是“浅层”的。修改嵌套对象的属性(如
this.obj.a.b = 1)往往无法触发 UI 刷新,除非你手动用ObjectLink或重新赋值整个对象。 - V2 的进化:V2 引入了深度观察能力。通过新的装饰器,框架能更精准地感知数据变化,从而实现更细粒度的 UI 更新,大幅减少不必要的重绘。
2. 装饰器对照表 (Cheatsheet)
这是最直接的变化,请牢记这张映射表:
|
功能 |
V1 (@Component) |
V2 (@ComponentV2) |
核心区别 |
|
组件声明 |
|
|
V2 组件不再支持继承其他组件 |
|
内部状态 |
|
|
|
|
外部参数 |
|
|
V2 默认单向传递。不再区分 Prop/Link。父组件传值,子组件只读。 |
|
双向绑定 |
|
|
V2 推荐通过事件回调修改父组件数据,或使用双向绑定语法糖 |
|
跨层级数据 |
|
|
别名更统一,能力更强 |
|
普通成员 |
(无装饰器) |
(无装饰器) |
V2 中普通变量变化绝对不会触发 UI 刷新 |
|
计算属性 |
(无直接对应) |
|
新增!类似于 Vue 的 computed,带缓存,依赖变化自动更新 |
|
监听变化 |
|
|
|
3. 生命周期:新增的关键一环
V1 中我们常苦恼于 aboutToAppear 时节点还没创建,无法操作底层。V2 完美解决了这个问题:
aboutToAppear:组件创建后,build执行前。适合初始化数据。onDidBuild(新增):build执行完毕,UI 节点创建完成。适合获取FrameNode、绑定手势、计算尺寸。aboutToDisappear:组件销毁前。
迁移建议:如果你在 V1 中使用 setTimeout(0) 来 hack 获取节点,请立即改用 V2 的 onDidBuild。
4. 实战代码对比
场景:父子组件传值与更新
V1 写法:
@Component
struct Child {
@Link count: number; // 双向绑定
build() {
Button(`Count: ${this.count}`)
.onClick(() => {
this.count++; // 直接修改,父组件同步更新
})
}
}
V2 写法:
@ComponentV2
struct Child {
@Param count: number = 0; // 只读!直接修改会报错
@Event changeCount: (val: number) => void; // 声明事件
build() {
Button(`Count: ${this.count}`)
.onClick(() => {
// this.count++; // 错误:@Param 是只读的
this.changeCount(this.count + 1); // 正确:通知父组件更新
})
}
}
// 父组件调用
// Child({ count: this.myCount, changeCount: (v) => this.myCount = v })
// 或者使用语法糖:
// Child({ count: this.myCount!! })
5. 核心痛点解决:实体对象变化的深度观察
这可能是 V1 和 V2 最大的体验分水岭。V1 是“整体替换”,V2 是“精准手术”。
V1 的痛点:必须“整体替换”或“强行包装”
在 V1 中,@State 装饰的对象,默认只能观察到第一层的变化。
假设你有一个实体:
class User {
name: string = "Jack"
age: number = 18
}
@Component
struct UserCard {
@State user: User = new User()
build() {
Column() {
Text(this.user.name)
Button("变老").onClick(() => {
// 错误写法:直接修改属性,UI 不会刷新!
this.user.age++
// V1 正确写法 1:重新赋值整个对象(性能差,因为要创建新对象)
// this.user = new User(this.user.name, this.user.age + 1)
// V1 正确写法 2:使用 @Observed + @ObjectLink(繁琐,需要拆分组件)
// 必须把 User 类标记为 @Observed,且 UserCard 必须把 user 传给子组件,
// 子组件用 @ObjectLink 接收,才能在子组件里修改属性触发刷新。
})
}
}
}
V2 的进化:@ObservedV2 + @Trace 实现“深度监听”
V2 引入了真正的代理(Proxy)机制,它可以深入到对象的每一个属性。
同样的实体,在 V2 中这样写:
// 1. 给类打上 @ObservedV2 标签
@ObservedV2
class User {
// 2. 给需要观察的属性打上 @Trace 标签
@Trace name: string = "Jack"
@Trace age: number = 18
}
@ComponentV2
struct UserCard {
// 使用 @Local 接收
@Local user: User = new User()
build() {
Column() {
Text(this.user.name)
Button("变老").onClick(() => {
// V2 完美写法:直接修改属性,UI 自动刷新!
this.user.age++
})
}
}
}
核心对比表
|
操作场景 |
V1 (@State / @Observed) |
V2 (@Local / @ObservedV2) |
|
修改对象属性 |
不刷新 |
刷新 |
|
替换整个对象 |
刷新 |
刷新 |
|
嵌套对象修改 |
极难处理 |
轻松支持 |
|
数组元素修改 |
不刷新 |
刷新 |
6. 常见坑与避雷指南
- 混用禁止:同一个 Struct 只能用 V1 或 V2,不能同时加两个装饰器。但在同一个项目中,V1 和 V2 组件可以互相引用(父 V1 含子 V2,或反之)。
@Param默认值:V2 中@Param必须要在本地给默认值,或者标记为可选,否则编译报错。- 数组更新:V2 对数组的操作(push, splice 等)能被更好地捕获,但依然建议遵循不可变数据的最佳实践。
- 类对象:如果你自定义了 Class 并希望它能触发 UI 更新,记得给 Class 加上
@ObservedV2,给属性加上@Trace。这是 V2 深度观察的基石。
7. 总结
V2 牺牲了一点点“随手修改”的便利性(比如 @Link 的直接赋值),换来了更清晰的数据流和更强悍的性能。
- 新项目:强烈建议直接全量使用 V2。
- 老项目:可以渐进式迁移。先从性能瓶颈大的复杂列表项组件开始重构为 V2。
更多推荐




所有评论(0)