HarmonyOS ArkUI 状态管理全解:从 @State 到 @Observed 的选型与实战
引子:状态管理是 ArkUI 的命脉
写鸿蒙应用,90% 的 bug 都和状态管理有关——数据变了 UI 没更新、子组件拿不到父组件的值、对象属性改了但页面没刷新。ArkUI 提供了六种状态管理工具,每种解决不同的问题。选错了,代码会越来越乱;选对了,数据流清晰,bug 自然少。
今天就从实际开发场景出发,拆解 @State、@Prop、@Link、@Provide/@Consume、@Observed/@ObjectLink、@Watch 六个装饰器的用法、区别和选型逻辑。
完整效果
先搞懂一个问题:状态从哪来
ArkUI 是声明式 UI——UI 是状态的函数:UI = f(state)。状态变了,UI 自动更新。
但"状态存在哪里"是个问题:
- 只在当前组件用?→ 存组件内部
- 父子组件共享?→ 存父组件,传给子组件
- 祖孙多层传递?→ 存全局,跨层传递
- 对象的某个属性变了?→ 需要深度观察
六种装饰器对应四种场景,搞清楚场景就能选对工具。
@State:组件内部的状态
@Component
struct Counter {
@State count: number = 0;
build() {
Column() {
Text(`点击了 ${this.count} 次`)
Button('+1').onClick(() => { this.count++; })
}
}
}

作用域
@State 变量只在当前组件内有效。外部无法直接修改。
触发更新的条件
- 基本类型(number、string、boolean):值变化就触发
- 对象/数组:引用变化才触发(
this.obj = newObj),属性变化不触发
常见错误
// 错误:修改对象属性,UI 不更新
@State user: UserInfo = { name: '张三', age: 25 };
this.user.age = 26; // UI 不刷新!
// 正确:替换整个对象
this.user = { ...this.user, age: 26 }; // UI 刷新
适用场景
组件内部的私有状态——计数器、开关、输入框的值、当前选中的 Tab。
@Prop:父传子的单向绑定
// 父组件
@Component
struct Parent {
@State message: string = 'hello';
build() {
Column() {
Child({ msg: this.message })
Button('改消息').onClick(() => { this.message = 'world'; })
}
}
}
// 子组件
@Component
struct Child {
@Prop msg: string;
build() {
Text(this.msg)
}
}
数据流
Parent.message → Child.msg(单向:父→子)
父组件的 message 变了,子组件的 msg 自动更新。但子组件不能反过来修改父组件的 message。
@Prop 的拷贝语义
@Prop 会拷贝父组件的值。子组件拿到的是副本,修改副本不影响父组件。
// 子组件
@Prop msg: string;
this.msg = 'changed'; // 只改自己,父组件不受影响
适用场景
父组件传配置给子组件——标题文字、是否显示、颜色值。子组件只负责展示,不负责修改。
@Link:父子双向绑定
// 父组件
@Component
struct Parent {
@State count: number = 0;
build() {
Column() {
Child({ count: $count }) // 用 $ 传引用
Text(`父组件: ${this.count}`)
}
}
}
// 子组件
@Component
struct Child {
@Link count: number;
build() {
Button(`子组件 +1`).onClick(() => { this.count++; })
}
}
数据流
Parent.count ↔ Child.count(双向:父↔子)
子组件修改 count,父组件的 count 也会变。两边同步。
$ 符号的含义
Child({ count: $count }) // 传引用
Child({ count: this.count }) // 传值(错误!)
$count 传的是变量的引用,this.count 传的是值。用错了,双向绑定就失效。
@Link vs @Prop
| 特性 | @Prop | @Link |
|---|---|---|
| 数据流 | 父→子(单向) | 父↔子(双向) |
| 子组件修改 | 不影响父组件 | 同步到父组件 |
| 传值方式 | msg: this.value |
count: $value |
适用场景
子组件需要修改父组件的状态——表单输入、开关切换、列表项的选中状态。
@Provide/@Consume:跨层传递
// 祖先组件
@Component
struct Grandparent {
@Provide('theme') currentTheme: string = 'dark';
build() {
Column() {
Parent()
Button('切换主题').onClick(() => {
this.currentTheme = this.currentTheme === 'dark' ? 'light' : 'dark';
})
}
}
}
// 中间组件(不需要传递 theme)
@Component
struct Parent {
build() { Child() }
}
// 后代组件
@Component
struct Child {
@Consume('theme') currentTheme: string;
build() {
Text(`当前主题: ${this.currentTheme}`)
}
}
数据流
Grandparent.currentTheme → Child.currentTheme(跨层:祖先→后代)
中间的 Parent 组件不需要知道 theme 的存在,数据直接穿透传递。
为什么需要 key
@Provide('theme') currentTheme: string;
@Consume('theme') currentTheme: string;
'theme' 是 key,@Provide 和 @Consume 必须用同一个 key 才能匹配。一个组件可以 Provide 多个值,用不同的 key 区分。
适用场景
全局配置——主题、语言、用户信息、登录状态。不需要层层传递,直接跨层获取。
@Observed/@ObjectLink:对象深度观察
@Observed
class UserInfo {
name: string = '';
age: number = 0;
}
// 父组件
@Component
struct Parent {
@State user: UserInfo = new UserInfo();
build() {
Column() {
Child({ user: this.user })
Button('改年龄').onClick(() => { this.user.age++; })
}
}
}
// 子组件
@Component
struct Child {
@ObjectLink user: UserInfo;
build() {
Text(`${this.user.name} ${this.user.age}`)
}
}
@Observed 的作用
给类加 @Observed,框架会深度观察这个类的实例。当实例的属性变化时,引用这个实例的组件会更新。
@ObjectLink 的作用
子组件用 @ObjectLink 接收 @Observed 对象,就能观察到属性的变化。
@State + @Observed 的组合
// 父组件用 @State + @Observed 类
@State user: UserInfo = new UserInfo();
// 子组件用 @ObjectLink
@ObjectLink user: UserInfo;
父组件修改 this.user.age,子组件会更新。不需要替换整个对象。
为什么 @State 单独不行
@State 只观察引用变化,不观察属性变化。如果 UserInfo 没加 @Observed,this.user.age++ 不会触发子组件更新。
@Observed + @ObjectLink vs @State
| 场景 | 用 @State | 用 @Observed + @ObjectLink |
|---|---|---|
| 基本类型 | ✅ | 不需要 |
| 对象替换 | ✅ | 不需要 |
| 对象属性修改 | ❌ | ✅ |
| 父子共享对象 | ❌ | ✅ |
适用场景
列表项的内部状态——每个列表项是一个对象,修改某个项的属性(比如收藏、展开),其他项不受影响。
@Watch:监听状态变化
@Component
struct SearchBox {
@State @Watch('onKeywordChange') keyword: string = '';
onKeywordChange(): void {
console.log(`关键词变了: ${this.keyword}`);
// 触发搜索请求
}
build() {
TextInput({ text: this.keyword })
.onChange((v: string) => { this.keyword = v; })
}
}
@Watch 的触发时机
状态值变化时,自动调用 @Watch 指定的函数。在函数里可以做副作用——发请求、打日志、更新其他状态。
为什么不在 onChange 里直接处理
// 方案一:在 onChange 里处理
.onChange((v: string) => {
this.keyword = v;
this.doSearch(v); // 需要手动调用
})
// 方案二:用 @Watch
@State @Watch('onKeywordChange') keyword: string = '';
// 只要 keyword 变了,自动触发,不管从哪里改的
@Watch 的好处是"不管状态从哪里被修改,都会触发"。如果以后有其他地方修改 keyword(比如清空按钮),不需要重复写处理逻辑。
适用场景
搜索防抖、表单校验、日志记录、状态同步。
选型决策树
需要管理状态?
├─ 只在组件内部用 → @State
├─ 父子组件共享
│ ├─ 子组件只读 → @Prop
│ └─ 子组件要改 → @Link
├─ 跨多层传递 → @Provide/@Consume
├─ 对象属性要深度观察 → @Observed + @ObjectLink
└─ 状态变化要触发副作用 → + @Watch
六种装饰器的对比表
| 装饰器 | 作用域 | 数据流 | 修改权 | 适用场景 |
|---|---|---|---|---|
| @State | 组件内 | 组件内 | 组件内 | 私有状态 |
| @Prop | 父→子 | 单向 | 子可改(不影响父) | 配置传递 |
| @Link | 父↔子 | 双向 | 双方都可改 | 表单输入 |
| @Provide | 祖先→后代 | 跨层 | 祖先改,后代读 | 全局配置 |
| @Consume | 后代读祖先 | 跨层 | 后代读,祖先改 | 全局配置 |
| @Observed | 对象属性 | 深度观察 | 任何持有引用的组件 | 列表项状态 |
| @ObjectLink | 子组件读对象 | 深度观察 | 子组件可改 | 列表项状态 |
| @Watch | 任意 | 监听变化 | 触发回调 | 副作用处理 |
踩坑记录
坑 1:@Link 传值用了 this 而不是 $
// 错误
Child({ count: this.count });
// 正确
Child({ count: $count });
坑 2:@Observed 漏加
// 错误:UserInfo 没加 @Observed
class UserInfo { name: string = ''; }
// 正确
@Observed
class UserInfo { name: string = ''; }
坑 3:@Provide 和 @Consume 的 key 不一致
// 祖先
@Provide('theme') currentTheme: string;
// 后代
@Consume('color') currentTheme: string; // key 是 'color',匹配不上!
坑 4:@Prop 没有默认值
// 错误:@Prop 没有默认值,编译报错
@Prop msg: string;
// 正确:给默认值
@Prop msg: string = '';
坑 5:@Watch 的函数名拼错
@State @Watch('onChage') keyword: string = ''; // 函数名拼错,不会报错但不会触发
坑 6:@State 数组的更新
// 错误:push 不触发更新
this.items.push(newItem);
// 正确:用展开运算符
this.items = [...this.items, newItem];
代码改进建议
1. 状态下沉
把状态放在"需要它的最小组�件"里。如果只有子组件用,不要放父组件。
2. 避免过度使用 @Provide
@Provide 是全局状态,用多了会让数据流难以追踪。优先用 @Prop/@Link 传参。
3. 用 @Watch 替代多处 onChange
如果一个状态变化需要在多个地方响应,用 @Watch 集中处理,比分散在各处的 onChange 更好维护。
4. @Observed 对象的不可变更新
虽然 @Observed 支持属性修改,但用不可变更新(替换对象)更安全,不容易出 bug。
5. 状态管理的测试
每个 @State 变量都应该有对应的测试用例,验证状态变化是否触发预期的 UI 更新。
总结
ArkUI 的六种状态管理工具解决四种场景:组件内部(@State)、父子传递(@Prop/@Link)、跨层传递(@Provide/@Consume)、对象深度观察(@Observed/@ObjectLink)。@Watch 是通用的副作用触发器,可以搭配任何装饰器使用。
适用边界:这个部分适合用作 ArkUI 状态管理的系统性学习,涵盖了六个装饰器的用法、区别、选型、踩坑和最佳实践。但如果要深入某个具体装饰器(比如 @Observed 的实现原理、@Provide 的依赖注入机制),还需要更底层的分析。
说白了,状态管理就是"把数据放在对的地方"。放在对的地方,UI 自动更新;放在错的地方,bug 满天飞。
更多推荐



所有评论(0)