HarmonyOS 状态管理 V2 实战教程
结合真实开发,按使用频率逐个讲透 V2 全部装饰器:@Local、@Param、@Event、@ObservedV2、@Trace、@Computed、@Monitor、@Provider、@Consumer、@Once,以及 !! 双向绑定。
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,可维护性更高。
@ComponentV2 装饰(不能用 @Component)。@Entry 组件也要加 @ComponentV2。02装饰器全景图
| V2 装饰器 | 作用 | V1 对应 | 使用频率 |
|---|---|---|---|
@ComponentV2 | V2 组件入口 | @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使用频率排行(结合实战)
下面按真实开发中的使用频次排序,后面章节也按这个顺序逐个拆解。数值为相对热度,便于你分配学习精力。
04@Local —— 组件内部状态
取代 V1 的 @State(仅内部状态那部分)。被 @Local 装饰的变量是组件私有状态,只能组件内部初始化和修改,不允许从父组件传值(否则编译报错)。
基本用法
@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。
基本用法
@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 把「必填项」钉死在编译期,调用方漏传直接红波浪,比运行时才发现的坑强太多。06@ObservedV2 / @Trace —— 深度观测
这是 V2 的「杀手锏」。V1 里嵌套对象要逐层 @ObjectLink,代码又臭又长;V2 用 @ObservedV2 装饰类、@Trace 装饰属性,任意层级嵌套属性变化都能精准刷新 UI。
基本用法(嵌套也能观测)
@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
@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 } })
}
}
}写法二:!! 语法糖(等价,更简洁)
@Entry
@ComponentV2
struct Page {
@Local num: number = 0;
build() {
Column() {
// this.num!! 自动展开为 { value: this.num, $value: v => this.num = v }
Stepper({ value: this.num!! })
}
}
}!! 也用于系统组件参数双向绑定,例如 bindMenu 的 isShow、bindPopup 的 show、bindContextMenu 的 isShown(API 13+)。!! 不支持多层父子组件传递;事件方法名必须严格为 $ + @Param 属性名。08@Computed —— 计算属性
对标 Vue 的 computed:声明一个 get 派生属性,依赖变化时才重算一次,多处复用时不会重复计算,解决性能问题。
基本用法
@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,但强得多:可按属性路径精确监听、能拿到变化前 / 后的值、可同时监听多个变量。
基本用法(拿新旧值)
@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 真正双向)
@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}`) // 任意一方修改,另一方同步
}
}
}@Consumer必须本地初始化(找不到@Provider时用默认值)。- alias(别名)是唯一匹配 key,缺省时默认用属性名。
- 支持装饰函数类型(V1 不支持),可把回调也跨层传。
@Provider默认支持重名(就近查找最近的提供方)。
@Param 透传的优先透传。11@Once —— 一次性初始化
与 @Param 联用:只在初始化时从父组件取一次值,之后父组件再变也不会同步。适合「配置快照 / 一次性参数」。
基本用法
@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 列表(点击展开)
@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 迁移速查
| V1 | V2 | 说明 |
|---|---|---|
@Component | @ComponentV2 | 组件入口换成 V2 |
@State | @Local | 仅内部状态;需外部传值用 @Param |
@Prop | @Param | 单向、只读 |
@Link | @Param + @Event(或 !!) | 显式双向 |
@Observed + @ObjectLink | @ObservedV2 + @Trace | 深度观测、属性级更新 |
@Track | @Trace | 精准观察 |
@Watch | @Monitor | 可拿新旧值、按路径监听 |
@Provide / @Consume | @Provider / @Consumer | 真正双向、需本地初始化 |
$$ | !! | 双向绑定语法糖 |
AppStorage | AppStorageV2 | 全局状态,解耦持久化 |
PersistentStorage | PersistenceV2 | 持久化能力独立可用 |
@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的方法调用即可观测,无需整体替换。