HarmonyOS ArkTS 状态管理实践:@State、@Prop、@Link 与 @Observed 的取舍
一、为什么单独聊状态管理
在 ArkTS 里写页面,最容易出问题的不是布局,而是「数据改了界面没变」。根因通常只有一个:你改的那个变量,不在框架的观察范围内。
ArkTS 的状态管理本质上是一套「可观察性」规则:只有被装饰器标记过、并且按规则赋值的变量,变化才会触发 UI 重建。搞清楚这条线,绝大多数「不刷新」问题都能自己定位。
二、@State:组件自己的状态
@State 是最基础的一层,特点有三个:
- 私有:只能在当前组件内部初始化,父组件不能直接给它赋值(只能通过构造参数初始化一次)。
- 本地:生命周期跟随组件,组件销毁即释放。
- 可观察:赋值动作会触发该组件重建。
@Entry
@Component
struct Counter {
@State count: number = 0;
build() {
Column() {
Text(`count = ${this.count}`)
Button('+1').onClick(() => { this.count++ })
}
}
}
关键点在「赋值动作」四个字:this.count++ 是重新赋值,能触发刷新;而 this.obj.a = 1(修改对象内部属性)不会触发,因为 @State 观察的是 obj 这个引用,不是它的字段。这就是后面 @Observed 要解决的问题。
三、@Prop 与 @Link:父子之间怎么传
父子传值只有两条路,取舍其实很清楚:
| 装饰器 | 方向 | 子组件修改 | 适用场景 |
|---|---|---|---|
@Prop |
父 → 子 单向 | 允许,但不回写父 | 子组件只读展示,需要本地临时状态 |
@Link |
父 ↔ 子 双向 | 会同步回父组件 | 表单、开关、步进器等需要回写的控件 |
@Prop 是值拷贝:父组件状态变化会重新同步给子组件,但子组件改了不会影响父组件。适合「父组件给一份数据,子组件自己玩」的场景。
@Link 是引用共享:子组件必须用 $ 前缀绑定父组件的 @State。
@Component
struct Stepper {
@Link value: number; // 双向
@Prop step: number = 1; // 单向
build() {
Row() {
Button('-').onClick(() => { this.value -= this.step })
Text(`${this.value}`)
Button('+').onClick(() => { this.value += this.step })
}
}
}
@Entry
@Component
struct Page {
@State total: number = 0;
build() {
Column() {
Text(`总计 ${this.total}`)
Stepper({ value: $total, step: 2 })
}
}
}
一个常见的踩坑:把 @Link 用在深层嵌套的对象上。由于 @Link 同样只观察引用,父组件里对对象字段的修改未必能同步过来,这时应该配合 @Observed 使用。
四、@Observed + @ObjectLink:观察嵌套对象
当状态是 class 实例、并且需要观察它的属性变化时,两步走:
- 给类加
@Observed,让框架能代理它的字段; - 子组件用
@ObjectLink接收这个实例。
@Observed
class User {
name: string = '';
age: number = 0;
}
@Component
struct UserCard {
@ObjectLink user: User;
build() {
Column() {
Text(this.user.name)
Button('改名').onClick(() => { this.user.name = 'new name' })
}
}
}
注意 @ObjectLink 只能接收 @Observed 修饰的类实例,不能接收普通对象字面量,也不能接收 undefined。这是编译期就会报错的硬约束。
五、跨层级:@Provide / @Consume
层级深了以后,一层层透传 @Prop 会很难维护。@Provide / @Consume 就是给这种场景准备的:祖先提供,任意层级后代消费,按变量名匹配,不需要中间组件参与。
@Entry
@Component
struct Root {
@Provide('theme') theme: string = 'light';
build() { PageBody() }
}
@Component
struct PageBody {
@Consume('theme') theme: string;
build() { Text(this.theme) }
}
代价是可读性:变量名一改,消费端就断了,而且从消费端反向找不到提供方。层级不深时,老老实实传参反而更好维护。
六、几条实践建议
- 能用 @State 就别上全局。
AppStorage/LocalStorage适合真正的跨页面共享(登录态、主题),不要当成懒人通道。 - 状态尽量扁平。嵌套越深,可观察性越难保证,维护成本呈指数上升。
- @Watch 只做副作用,不要在回调里再改回被观察的变量,很容易写出循环更新。
- API 12 之后再看 V2。
@ObservedV2+@Trace的深度观察能力更强,但 V1 / V2 不能在同一组件里混用,迁移要成体系地做,不建议新旧混写。
七、小结
一句话记住取舍:
@State管自己,@Prop单向给,@Link双向改,@Observed+@ObjectLink管对象内部,@Provide/@Consume管跨层级。
把「谁持有状态、谁只读、谁需要回写」这三件事在写代码前想清楚,状态管理基本不会成为返工点。
- 点赞
- 收藏
- 关注作者
评论(0)