当前位置:首页>鸿蒙APP>鸿蒙 ArkUI:V1 与 V2 装饰器全面对比与迁移指南

鸿蒙 ArkUI:V1 与 V2 装饰器全面对比与迁移指南

  • 2026-09-03 22:40:50
鸿蒙 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 写法

@Componentstruct MyCard {  @State 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

@Componentstruct Counter {  @State 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(双向)

// 父组件@Componentstruct Parent {  @State value: string = 'hello'  build() {    Column() {      // @Prop 单向传入,子组件修改不影响父      ChildA({ value: this.value })      // @Link 双向绑定,子组件修改同步到父      ChildB({ value: {this.user.age}`)    }  }}

V1 的问题:修改 user.age 会导致显示 name 的 Text 也重渲染,即使 name 没变。

V2 — @ObservedV2 + @Trace(字段级)

@ObservedV2class UserInfo {  @Trace name: string = ''   // 只有这个字段变化才触发相关组件更新  @Trace age: number = 0     // 独立追踪,互不影响  role: string = 'user'      // 未标 @Trace,变化不触发更新}@ComponentV2struct UserCard {  @Param user: UserInfo = new UserInfo()  build() {    Column() {      Text(this.user.name)      // 只在 name 变化时更新      Text(`{this.selectedIndex}`)      .onClick(() => this.selectedIndex = 1)  }}

差异对比:

特性
@Provide/@Consume(V1)
@Provider/@Consumer(V2)
命名方式
字符串 key
默认变量名(可指定别名)
修改方向
双向(Consume 可直接改)
双向(Consumer 可直接改)
初始值
Consume 不设初始值
Consumer 必须设默认值(Provider 未找到时使用)

七、监听变化:@Watch vs @Monitor

V1 — @Watch

@Componentstruct Page {  @State @Watch('onCountChange') count: number = 0  onCountChange() {    console.log('count 变了:', this.count)  }  build() { /* ... */ }}

V2 — @Monitor

@ComponentV2struct 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: {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

@Componentstruct Page {  @State firstName: string = '张'  @State lastName: string = '三'  // 普通 getter,每次访问都重新计算  get fullName(): string {    return this.firstName + this.lastName  }  build() {    Text(this.fullName)  }}

V2 — @Computed

@ComponentV2struct 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())// 组件中读取(双向绑定)@Componentstruct Page {  @StorageLink('userInfo') userInfo: UserInfo = new UserInfo()}

V2 — AppStorageV2

// 存入/获取(connect 模式,不存在则用工厂函数创建)const userInfo = AppStorageV2.connect(UserInfo, () => new UserInfo())!// 组件中读取@ComponentV2struct 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

@ObservedV2class Config {  theme: string = 'light'       // ❌ 没有 @Trace,修改不触发 UI 更新  @Trace language: string = 'zh'  // ✅ 有 @Trace,修改触发更新}

踩坑三:V1/V2 组件混用

@ComponentV2struct MyComp {  @State count: number = 0  // ❌ V2 组件内不能用 @State  @Local count: number = 0  // ✅ 正确}

十二、总结

V2 体系的核心设计理念是:明确的数据流方向 + 精准的响应粒度 + 清晰的组件契约。

  • 单向数据流
    :通过删除 @Link、强制使用 @Event,让数据流向一目了然
  • 精准响应
    :@ObservedV2 + @Trace 实现字段级响应,大幅减少不必要的重渲染
  • 类型安全
    :AppStorageV2、@Require @Param 等让组件接口更加明确

如果你正在开发新的鸿蒙应用,建议直接采用 V2 体系;如果是维护老项目,也可以按模块逐步迁移,两套体系可以在应用层面共存。


最新文章

随机文章