从 @Link 迁移到 V2:三种优雅实现双向同步的方案

前言

HarmonyOS 状态管理 V2(@ComponentV2 / @ObservedV2)发布后,很多同学把 V1 的 @Link 直接替换成 @Param + @Event,发现 @Param 是只读的,必须手写回调把子组件的变更通知父组件。结果代码量翻倍,嵌套层级深时事件还要层层透传,非常痛苦。

其实 V2 并没有“退步”,而是把数据流设计得更显式。本文用 3 个官方提供的武器,按场景选型,让你在大多数情况下代码量与 V1 持平甚至更少

问题描述

典型痛点有两个:

  1. 代码膨胀@Param 只读,子组件要改值必须配一个 @Event 回调,父组件再写回,一个简单的计数器要多写 5 行胶水代码。
  2. 深层传递:组件嵌套 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

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐