状态管理 V2

HarmonyOS 状态管理 V2 实战教程

结合真实开发,按使用频率逐个讲透 V2 全部装饰器:@Local、@Param、@Event、@ObservedV2、@Trace、@Computed、@Monitor、@Provider、@Consumer、@Once,以及 !! 双向绑定。

API 12+ @ComponentV2 按频率排序 深度观测

01为什么是 V2

ArkUI 状态管理 V2 从 API 12 开始提供,是对 V1 的增强。官方建议:新项目直接用 V2;老 V1 项目若功能/性能够用可不迁移,需要深度观测、属性级更新时再渐进迁移。

V2 相比 V1 的核心改进

  • 状态源更清晰:明确区分「组件内部状态 @Local」与「外部输入 @Param」,组件边界一目了然。
  • 深度观测:@ObservedV2 + @Trace 让任意层级嵌套对象的属性变化都能自动刷新 UI,不再需要 V1 里逐层 @ObjectLink。
  • 属性级精准更新:只刷新依赖该属性的组件,避免无关属性变更导致的整对象重渲染,性能更好。
  • 内置类型代理:Date / Map / Set 调用原生方法(如 map.set())即可被观测。
  • 显式的数据流:用「父传子 @Param + 子传父 @Event」替代 V1 隐式的 @Link,可维护性更高。
前提:使用 V2 装饰器,组件必须用 @ComponentV2 装饰(不能用 @Component)。@Entry 组件也要加 @ComponentV2。

02装饰器全景图

V2 装饰器作用V1 对应使用频率
@ComponentV2V2 组件入口@Component必备
@Local组件内部私有状态(禁止外部初始化)@State极高
@Param父→子输入,子组件只读@Prop极高
@ObservedV2 / @Trace类/属性深度观测@Observed / @ObjectLink / @Track极高
@Event子→父事件输出自定义回调高
!!双向绑定语法糖(@Param + @Event)$$高
@Computed计算属性(带缓存)—高
@Monitor状态变量深度监听(可拿新旧值)@Watch中
@Provider / @Consumer跨组件层级双向同步@Provide / @Consume中
@Once配合 @Param,仅初始化同步一次—低
@Require标记 @Param 为必传(编译期校验)—低
高 / 极高 日常高频 中 / 低 按需使用

03使用频率排行(结合实战)

下面按真实开发中的使用频次排序,后面章节也按这个顺序逐个拆解。数值为相对热度,便于你分配学习精力。

@Local
极高
@Param / @Require
极高
@ObservedV2 / @Trace
极高
@Event + !!
高
@Computed
高
@Monitor
中
@Provider / @Consumer
中
@Once
低
@Require
低
学习建议:前三个(@Local / @Param / @ObservedV2+@Trace)覆盖 90% 日常需求;@Event、!!、@Computed 是中高频组合技;@Monitor、@Provider/@Consumer 在需要「联动副作用 / 跨层共享」时再上;@Once、@Require 属于锦上添花。

04@Local —— 组件内部状态

取代 V1 的 @State(仅内部状态那部分)。被 @Local 装饰的变量是组件私有状态,只能组件内部初始化和修改,不允许从父组件传值(否则编译报错)。

基本用法

ArkTS
@Entry
@ComponentV2
struct Counter {
  @Local count: number = 0;        // 基本类型:赋值即刷新
  @Local tags: string[] = [];      // 数组:push / splice / 整体替换均可观测

  build() {
    Column({ space: 12 }) {
      Text(`当前计数:${this.count}`).fontSize(22)
      Button('+1').onClick(() => { this.count++ })   // 自动刷新 UI
      Button('加标签').onClick(() => this.tags.push('new'))
    }
    .padding(20)
  }
}
易错点:@Local 禁止外部初始化:Counter({ count: 5 }) 会编译失败。需要外部传值请用 @Param。

观测规则速记

  • 基本类型:赋值即刷新(this.count = 10)。
  • 对象类型:整体替换可观测(this.info = new Info());修改成员属性需配合 @ObservedV2 + @Trace。
  • 数组:基本类型数组任意修改可观测;对象数组改元素属性需 @Trace。
  • 内置类型 Date / Map / Set:调用原生 API(如 setFullYear()、map.set())即可观测。

05@Param / @Require —— 组件输入

@Param 取代 V1 的 @Prop:用于接收父组件传入的参数,单向同步(父→子),且子组件内只读(直接赋值会编译报错)。需要必传校验时叠加 @Require。

基本用法

ArkTS
@ComponentV2
struct TitleBar {
  @Require @Param title: string;            // 必传:不传编译报错
  @Param subtitle: string = '默认副标题';   // 可选:带默认值

  build() {
    Column() {
      Text(this.title).fontSize(24).fontWeight(700)
      Text(this.subtitle).fontColor(Color.Gray)
    }
  }
}

@Entry
@ComponentV2
struct Page {
  @Local title: string = '首页';
  build() {
    Column() {
      TitleBar({ title: this.title })   // @Param 单向:父变→子变
    }
  }
}
注意:子组件不能改 @Param(如 this.title = 'x' 报错)。要改父组件的值,走 @Event(下一节)或用 @Once 让子组件自己接管一份本地副本。
@Require 实战价值:写公共组件 / UI 组件库时,用 @Require 把「必填项」钉死在编译期,调用方漏传直接红波浪,比运行时才发现的坑强太多。

06@ObservedV2 / @Trace —— 深度观测

这是 V2 的「杀手锏」。V1 里嵌套对象要逐层 @ObjectLink,代码又臭又长;V2 用 @ObservedV2 装饰类、@Trace 装饰属性,任意层级嵌套属性变化都能精准刷新 UI。

基本用法(嵌套也能观测)

ArkTS
@ObservedV2
class Address {
  @Trace city: string = '';
}

@ObservedV2
class User {
  @Trace name: string = '';
  @Trace age: number = 0;
  @Trace address: Address = new Address();   // 嵌套对象,照样深度观测
}

@Entry
@ComponentV2
struct Profile {
  @Local user: User = new User();

  build() {
    Column({ space: 10 }) {
      Text(`${this.user.name} / ${this.user.age}`)
      Text(`城市:${this.user.address.city}`)
      Button('改名').onClick(() => { this.user.name = '张三' })          // 属性级最小更新
      Button('换城市').onClick(() => { this.user.address.city = '北京' }) // 深层修改也能刷新
    }
    .padding(20)
  }
}
性能要点:只刷新真正依赖 user.address.city 的 Text,其它 UI 不动。这就是「属性级精准更新」,V1 的 @State 做不到。
易错点:只有被 @Trace 装饰的属性才具备深度观测能力;忘记加 @Trace 的属性改了不会刷新(除非整体替换对象)。

07@Event 与 !! 语法糖 —— 双向通信

V2 把「子传父」标准化为 @Event:装饰一个方法类型的变量,子组件通过调用它把变化「通知」父组件。配合 @Param 即可实现双向同步,比 V1 隐式 @Link 更清晰。

写法一:显式 @Param + @Event

ArkTS
@ComponentV2
struct Stepper {
  @Param value: number = 0;
  @Event $value: (v: number) => void = () => {};   // 事件名必须是 $ + Param 名

  build() {
    Row({ space: 10 }) {
      Button('-').onClick(() => this.$value(this.value - 1))
      Text(`${this.value}`)
      Button('+').onClick(() => this.$value(this.value + 1))
    }
  }
}

@Entry
@ComponentV2
struct Page {
  @Local num: number = 0;
  build() {
    Column() {
      Stepper({ value: this.num, $value: (v) => { this.num = v } })
    }
  }
}

写法二:!! 语法糖(等价,更简洁)

ArkTS
@Entry
@ComponentV2
struct Page {
  @Local num: number = 0;
  build() {
    Column() {
      // this.num!! 自动展开为 { value: this.num, $value: v => this.num = v }
      Stepper({ value: this.num!! })
    }
  }
}
!! 还能用于内置组件:V2 中 !! 也用于系统组件参数双向绑定,例如 bindMenu 的 isShow、bindPopup 的 show、bindContextMenu 的 isShown(API 13+)。
约束:!! 不支持多层父子组件传递;事件方法名必须严格为 $ + @Param 属性名。

08@Computed —— 计算属性

对标 Vue 的 computed:声明一个 get 派生属性,依赖变化时才重算一次,多处复用时不会重复计算,解决性能问题。

基本用法

ArkTS
@Entry
@ComponentV2
struct Cart {
  @Local items: number[] = [10, 20, 30];

  @Computed
  get total(): number {
    return this.items.reduce((s, v) => s + v, 0);   // 仅依赖变化时重算
  }

  @Computed
  get isEmpty(): boolean {
    return this.items.length === 0;
  }

  build() {
    Column({ space: 10 }) {
      Text(`合计:${this.total}`)
      Text(`状态:${this.isEmpty ? '空' : '有货'}`)
      Button('加一条').onClick(() => this.items.push(50))
    }
    .padding(20)
  }
}
规则:@Computed 必须用 get 声明、只读;内部只能依赖已被状态装饰器(@Local / @Param / @Trace 等)管理的变量。

09@Monitor —— 深度监听

取代 V1 的 @Watch,但强得多:可按属性路径精确监听、能拿到变化前 / 后的值、可同时监听多个变量。

基本用法(拿新旧值)

ArkTS
@ObservedV2
class Form {
  @Trace keyword: string = '';
  @Trace page: number = 1;
}

@Entry
@ComponentV2
struct SearchPage {
  @Local form: Form = new Form();

  // 监听深层属性(路径写法),任意一项变化都会触发
  @Monitor('form.keyword', 'form.page')
  onFormChange(monitor: IMonitor) {
    monitor.dirty.forEach((path: string) => {
      console.info(`${path}: ${monitor.value(path)?.before} -> ${monitor.value(path)?.now}`);
    });
    // 这里发起搜索请求……
  }

  build() {
    Column() {
      TextInput({ text: this.form.keyword })
        .onChange(v => this.form.keyword = v)
    }
    .padding(20)
  }
}
被监听的变量必须被装饰:@Monitor 只能监听被 @Local / @Param / @Provider / @Consumer / @Computed 装饰的变量;监听对象里的属性变化时,属性要被 @Trace 装饰,否则监听不到。
更多路径写法:数组项 @Monitor('list.0.name');数组长度 @Monitor('list.length');类内部也能用 @Monitor 监听自身 @Trace 属性。

10@Provider / @Consumer —— 跨层共享

取代 V1 的 @Provide / @Consume,用于跨组件层级的双向同步(主题、登录用户、全局配置等),不用一层层透传 props。

基本用法(V2 真正双向)

ArkTS
@Entry
@ComponentV2
struct Root {
  @Provider('theme') theme: string = 'dark';   // 提供方:必须本地初始化

  build() {
    Column() {
      Button('切换主题')
        .onClick(() => { this.theme = this.theme === 'dark' ? 'light' : 'dark' })
      Child()
    }
  }
}

@ComponentV2
struct Child {
  @Consumer('theme') theme: string = 'light';  // 消费方:有默认值兜底

  build() {
    Column() {
      Text(`当前主题:${this.theme}`)   // 任意一方修改,另一方同步
    }
  }
}
V2 vs V1 差异:
  • @Consumer 必须本地初始化(找不到 @Provider 时用默认值)。
  • alias(别名)是唯一匹配 key,缺省时默认用属性名。
  • 支持装饰函数类型(V1 不支持),可把回调也跨层传。
  • @Provider 默认支持重名(就近查找最近的提供方)。
注意:这俩装饰器把组件「粘合」在一起,从组件独立/复用角度考虑应减少滥用;能靠 @Param 透传的优先透传。

11@Once —— 一次性初始化

与 @Param 联用:只在初始化时从父组件取一次值,之后父组件再变也不会同步。适合「配置快照 / 一次性参数」。

基本用法

ArkTS
@ComponentV2
struct ConfigPanel {
  @Once @Param initialCount: number = 0;   // 仅首次从父组件取值
  @Local count: number = this.initialCount;

  build() {
    Column() {
      Text(`初始快照:${this.initialCount}`)
      Text(`当前:${this.count}`)
    }
  }
}
等价理解:@Once @Param ≈ @Local + 父组件首次传值。常用于把外部配置「冻结」成组件内部状态。

12综合实战:Todo 列表

把高频装饰器串起来:@Local 管列表、@ObservedV2+@Trace 管条目、@Param+@Event 做子项编辑、@Computed 算剩余、@Monitor 打日志。

完整示例:Todo 列表(点击展开)
ArkTS
@ObservedV2
class Todo {
  @Trace text: string = '';
  @Trace done: boolean = false;
}

@ComponentV2
struct TodoItem {
  @Param item: Todo = new Todo();
  @Event $item: (t: Todo) => void = () => {};

  build() {
    Row({ space: 10 }) {
      Checkbox().select(this.item.done)
        .onChange(v => { this.item.done = v; this.$item(this.item); })
      Text(this.item.text)
        .decoration({ type: this.item.done ? TextDecorationType.LineThrough : TextDecorationType.None })
    }
  }
}

@Entry
@ComponentV2
struct TodoApp {
  @Local list: Todo[] = [new Todo(), new Todo()];
  @Local input: string = '';

  @Computed get left(): number {
    return this.list.filter(t => !t.done).length;
  }

  @Monitor('list')
  logChange(m: IMonitor) {
    console.info('列表变化:', m.value()?.now?.length);
  }

  build() {
    Column({ space: 12 }) {
      Text(`待办 ${this.list.length} · 剩余 ${this.left}`).fontSize(18)
      TextInput({ text: this.input!! }).onChange(v => this.input = v)
      Button('添加').onClick(() => {
        const t = new Todo(); t.text = this.input; this.list.push(t); this.input = '';
      })
      ForEach(this.list, (item: Todo) => {
        TodoItem({ item: item!! })
      })
    }
    .padding(20)
  }
}

13V1 → V2 迁移速查

V1V2说明
@Component@ComponentV2组件入口换成 V2
@State@Local仅内部状态;需外部传值用 @Param
@Prop@Param单向、只读
@Link@Param + @Event(或 !!)显式双向
@Observed + @ObjectLink@ObservedV2 + @Trace深度观测、属性级更新
@Track@Trace精准观察
@Watch@Monitor可拿新旧值、按路径监听
@Provide / @Consume@Provider / @Consumer真正双向、需本地初始化
$$!!双向绑定语法糖
AppStorageAppStorageV2全局状态,解耦持久化
PersistentStoragePersistenceV2持久化能力独立可用
混用规则:V1 与 V2 装饰器不能在同一组件内混用(如 @ComponentV2 里不能用 @State)。需要渐进迁移时,按组件边界拆分,官方提供混用与迁移指南。

14最佳实践与避坑清单

  • 新组件统一 @ComponentV2:避免 V1/V2 混用报错。
  • 状态源分清内外:内部用 @Local,外部传值用 @Param,别再无脑 @State。
  • 复杂模型用 @ObservedV2+@Trace:业务实体类一律加,嵌套修改才刷新。
  • 双向优先 @Param+@Event / !!:比 @Link 数据流更清晰。
  • 派生展示用 @Computed:列表筛选、合计、是否空等,避免重复计算。
  • 副作用用 @Monitor:发请求、动画、埋点;监听对象属性记得 @Trace。
  • 跨层共享克制使用 @Provider/@Consumer:能 @Param 透传就别跨层。
  • 公共组件必填项加 @Require:把错误挡在编译期。
  • !! 不跨多层:仅在直接父子间使用。
  • 内置类型直接调原生 API:Date/Map/Set 的方法调用即可观测,无需整体替换。

15官方文档

HarmonyOS 状态管理 V2 实战教程 · 响应式 · 深浅色自适应

基于 API 12+ 官方文档整理,示例用于说明用法,实际项目请以官方最新文档为准。