鸿蒙 ArkUI 状态管理实战:AppStorage、@StorageLink 与 @Prop 的边界与选型
鸿蒙 ArkUI 状态管理实战:AppStorage、@StorageLink 与 @Prop 的边界与选型
写 ArkTS 应用到第三个页面时,状态管理基本都会翻车一次:数据改了 UI 不动,或者 UI 动了数据没落盘,或者父组件改了子组件不刷新。这篇把 ArkUI 里最常混用的几个装饰器和 AppStorage 讲清楚,附一套可以直接抄的选型判断。
一、先分清三种状态归属
很多人把 @State、@Prop、@Link、@StorageLink 当成同一层的东西,其实它们回答的是不同问题:
- @State:组件自己的状态。谁声明谁负责,出了这个组件就没人认识它。
- @Prop / @Link:父子之间的单向/双向同步。数据的所有权仍在父组件。
- @StorageLink / @StorageProp:应用级状态,挂在 AppStorage 上,任何组件都能读写,且可跨页面存活。
一句话选型:只在当前组件用 → @State;只在父子间传 → @Prop/@Link;跨页面、跨组件共享 → AppStorage + @StorageLink。
二、AppStorage 到底是什么
AppStorage 是应用启动后就存在的单例键值容器,生命周期跟着 UIAbility 走,不跟着页面走。页面销毁重建,AppStorage 里的值还在。
// 写入(任意位置)
AppStorage.setOrCreate<string>('token', 'abc123')
AppStorage.setOrCreate<number>('cartCount', 0)
// 读取
const token: string | undefined = AppStorage.get<string>('token')
// 订阅变化(非组件上下文里用)
AppStorage.setAndLink('cartCount', 0).onChange((v: number) => {
console.info(`cartCount changed -> ${v}`)
})
关键点:AppStorage 存的是引用。存对象进去,改对象内部字段不会触发刷新,必须整体替换或者用 @Observed + @ObjectLink 处理嵌套。
三、@StorageLink 与 @StorageProp 的区别
@Entry
@Component
struct CartBadge {
// 双向:改它等于改 AppStorage
@StorageLink('cartCount') cartCount: number = 0
// 单向:AppStorage 变了会刷新它,但它变了不写回
@StorageProp('themeMode') themeMode: string = 'light'
build() {
Row() {
Text(`购物车 ${this.cartCount}`)
Text(`主题 ${this.themeMode}`)
}
}
}
@StorageLink 是双向绑定,本地赋值直接落到 AppStorage 并通知所有订阅者;@StorageProp 是只读投影,本地改它只影响自己,下次 AppStorage 变化会被覆盖。需要写回就用 Link,只需要读就用 Prop,别图省事全用 Link——多写入口会让状态来源不可追踪。
四、父子传值:@Prop 与 @Link 的坑
@Component
struct PriceTag {
@Prop price: number // 单向:父变子变,子变父不变
@Link discount: number // 双向:父子共用一份
build() {
Text(`${this.price * (1 - this.discount)}`)
}
}
@Entry
@Component
struct Detail {
@State price: number = 199
@State discount: number = 0.1
build() {
PriceTag({ price: this.price, discount: $discount })
}
}
两个必须记住的点:
@Link在父组件传参时要写$discount,不是this.discount,漏了$直接编译不过。@Prop是深拷贝。传对象进@Prop,子组件改内部字段父组件完全感知不到,而且大对象频繁刷新会有性能开销。对象跨层级共享请用@Observed+@ObjectLink。
五、真正容易翻车的三个场景
场景一:数组变了 UI 不刷新。
@State 装饰的数组,push / splice 这类会触发刷新,但 arr[i].name = 'x' 这种改元素内部字段不会。解法是整体替换:
this.list = this.list.map((it, i) => i === idx ? { ...it, name: 'x' } : it)
场景二:页面返回后数据丢失。
页面栈弹出再进来,@State 全归零。要在页面间保留,写进 AppStorage 或 PersistentStorage。需要跨进程重启还留,用 PersistentStorage.persistProp('token', ''),它会把值落到 Preferences。
场景三:在子组件里改了 @Prop,父组件没反应,还以为自己写错了。
这不是 bug,是设计。要双向就换 @Link,要子改父就传回调函数 onChange: (v: number) => void,显式比隐式好排查。
六、一张选型表
| 需求 | 用法 |
|---|---|
| 组件内部临时状态 | @State |
| 父传子只读 | @Prop |
| 父子双向共享一份 | @Link(父传 $xxx) |
| 跨页面/跨组件共享 | AppStorage + @StorageLink |
| 只订阅不写回 | @StorageProp |
| 对象嵌套字段变化要刷新 | @Observed + @ObjectLink |
| 应用重启后仍要保留 | PersistentStorage.persistProp |
七、实践建议
- AppStorage 只放真正全局的东西:登录态、主题、购物车数量、未读数。把页面局部状态塞进去,会让状态来源彻底失控。
- 键名统一加前缀(
user.token、cart.count),避免不同模块撞名。 - 退出登录时记得清理:
AppStorage.delete('user.token'),否则下一个账号进来会带着上一个的残留。 - 调试期在页面
aboutToAppear里打印AppStorage.keys(),一眼看出有哪些全局键,比猜快得多。
状态管理的复杂度不在 API 多少,而在每份数据只有一个明确的所有者。把归属想清楚,剩下的装饰器选择是自然结果。
- 点赞
- 收藏
- 关注作者
评论(0)