鸿蒙 ArkUI:V1 与 V2 装饰器全面对比与迁移指南
前言
在鸿蒙 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)
更多推荐




所有评论(0)