从 @Link 迁移到 V2:三种优雅实现双向同步的方案
从 @Link 迁移到 V2:三种优雅实现双向同步的方案
前言
HarmonyOS 状态管理 V2(@ComponentV2 / @ObservedV2)发布后,很多同学把 V1 的 @Link 直接替换成 @Param + @Event,发现 @Param 是只读的,必须手写回调把子组件的变更通知父组件。结果代码量翻倍,嵌套层级深时事件还要层层透传,非常痛苦。
其实 V2 并没有“退步”,而是把数据流设计得更显式。本文用 3 个官方提供的武器,按场景选型,让你在大多数情况下代码量与 V1 持平甚至更少。
问题描述
典型痛点有两个:
- 代码膨胀:
@Param只读,子组件要改值必须配一个@Event回调,父组件再写回,一个简单的计数器要多写 5 行胶水代码。 - 深层传递:组件嵌套 4、5 层时,状态要从祖父一路
@Param + @Event传到孙子,中间层全是被逼着“转发”的不相干代码。
更糟的是,很多人误以为“V2 不能双向同步了”,于是把 @Event 回调命名写错(V2 要求 $ + 参数名),导致数据回写失败。
细节解析
V2 的核心原则是:数据流向必须显式单向,但你可以用语法糖把“双向”的样板代码省掉。三种方案分别对应不同场景:
| 场景 | 推荐方案 | 说明 |
|---|---|---|
| 直接父子、简单类型 | !! 语法糖 | 一行替代 @Link,代码最省 |
| 多层嵌套、跨层级共享 | @Provider + @Consumer | 中间层零改动,彻底消除层层透传 |
| 传递对象、属性级修改 | @ObservedV2 + @Trace + @Param | 改属性即同步,无需 @Event |
| 跨页面 / 全局 | AppStorageV2 / PersistenceV2 | 独立 ViewModel,组件只订阅字段 |
关键规则:@Event 的方法名必须是 $ + @Param 属性名(例如 @Param value 对应 @Event $value),否则 !! 语法糖无法自动生成回写回调。
!! 语法糖从 API 12 开始支持,仅对直接父子有效;@Provider/@Consumer 同样 API 12 起支持。注意 @Provider/@Consumer 会让数据来源不再直观,复杂页面过度使用反而难追溯“谁改了数据”,简单父子场景仍建议显式 @Param + @Event。
示例代码
方案 A:!! 语法糖(直接父子最简)
@ComponentV2
struct Child {
@Param value: number = 0;
// @Event 命名规则:$ + @Param 名
@Event $value: (val: number) => void = (val: number) => {};
build() {
Column({ space: 10 }) {
Text(`子组件: ${this.value}`)
Button('子组件 +1')
.onClick(() => { this.$value(this.value + 1); })
}
}
}
@Entry
@ComponentV2
struct Parent {
@Local value: number = 0;
build() {
Column({ space: 20 }) {
Text(`父组件: ${this.value}`)
Button('父组件 +1').onClick(() => { this.value++; })
// !! 等价于 Child({ value: this.value, $value: (v) => { this.value = v; } })
Child({ value: this.value!! })
}
}
}
方案 B:@Provider + @Consumer(跨层级双向同步)
@ObservedV2
class AppState { @Trace count: number = 0; }
@Entry
@ComponentV2
struct Root {
@Provider('appCount') count: number = 0;
@Provider('appState') appState: AppState = new AppState();
build() {
Column({ space: 20 }) {
Text(`根组件: ${this.count}`)
Button('根组件 +1').onClick(() => { this.count++; })
Middle() // 中间层无需传递任何参数
}
}
}
@ComponentV2
struct Middle { build() { Child() } }
@ComponentV2
struct Child {
@Consumer('appCount') count: number = 0;
@Consumer('appState') appState: AppState = new AppState();
build() {
Column({ space: 10 }) {
Text(`深层子组件: ${this.count}`)
Button('深层 +1').onClick(() => { this.count++; }) // 直接改,自动回写 Provider
Button('对象属性 +1').onClick(() => { this.appState.count++; })
}
}
}
方案 C:@ObservedV2 + @Trace(对象属性级同步)
@ObservedV2
class FormData { @Trace name: string = ''; @Trace age: number = 0; }
@ComponentV2
struct FormItem {
@Param data: FormData = new FormData();
build() {
Column({ space: 10 }) {
Text(`姓名: ${this.data.name}`)
Button('改名').onClick(() => { this.data.name = '张三'; }) // 改属性即同步,无需 @Event
}
}
}
@Entry
@ComponentV2
struct FormPage {
@Local formData: FormData = new FormData();
build() {
Column({ space: 20 }) {
Text(`父 - 姓名: ${this.formData.name}`)
FormItem({ data: this.formData }) // 传对象引用即可
}
}
}
总结
迁移 @Link 不是“换个装饰器”,而是按数据流角色重新选型:
- 简单父子用
!!,代码量与 V1 持平; - 深层嵌套用
@Provider/@Consumer,中间层彻底解放; - 对象属性修改用
@ObservedV2+@Trace,体验最接近 V1 的@Observed+@ObjectLink,且是属性级精准刷新、性能更优。
三者可在同一 V2 工程组合使用。避坑要点:@Event 命名必须 $<Param名>、!! 仅限直接父子、跨页面共享走 AppStorageV2。
更多推荐
所有评论(0)