鸿蒙 ArkUI 状态管理实战:AppStorage、@StorageLink 与 @Prop 的边界与选型

举报
yd_288537773 发表于 2026/11/11 09:29:30 2026/11/11
【摘要】 ArkUI 状态管理选型指南:@State、@Prop、@Link、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 })
  }
}

两个必须记住的点:

  1. @Link 在父组件传参时要写 $discount,不是 this.discount,漏了 $ 直接编译不过。
  2. @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 多少,而在每份数据只有一个明确的所有者。把归属想清楚,剩下的装饰器选择是自然结果。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。