HarmonyOS6 PC 状态管理选型指南:什么场景用什么装饰器


被装饰器搞晕的不止你一个
我见过不少从其他框架转到 HarmonyOS6 PC 的开发者,第一反应都是:"这装饰器也太多了吧?"React 那边一个 useState 打天下,Vue 也就 ref 和 reactive 两个核心 API。到了 ArkUI 这里,@State、@Prop、@Link、@Provide、@Consume、@Observed、@ObjectLink、@StorageLink、@StorageProp、@Watch、@Builder、@BuilderParam、@Styles、@Extend……一长串下来,光记住名字就够费劲的,更别说搞清楚什么时候该用哪个了。
但说实话,这些装饰器的设计是有内在逻辑的。每个装饰器解决的都是一个具体的问题场景。今天这篇文章就是帮你建立一套"决策框架"——给你一个场景,你能快速判断该用什么方案。
先建立全局认知:状态管理的几个维度
在开始逐个讲解之前,有必要先理清 ArkUI 状态管理的几个维度。你遇到的所有选型问题,都可以从这几个维度来缩小范围:
数据流向——是组件自己的状态?还是从父组件传下来的?还是要传给子组件?还是要跨层级共享?
绑定方式——是单向的(只读)?还是双向的(可修改并回传)?
数据粒度——是整个对象的变化?还是对象内部某个属性的变化?
生命周期——是跟随组件的?还是跟随应用的?还是需要持久化到磁盘的?
复用需求——是 UI 结构的复用?还是样式的复用?
想清楚这几个维度,选型基本就确定了。
决策树:一张图找到你要的装饰器
下面是我总结的决策流程。遇到一个状态管理需求时,按顺序问自己这些问题:
需求是什么?
│
├─ 组件内部的状态(不涉及父子传递)
│ └─ @State
│
├─ 从父组件接收数据
│ ├─ 只读展示,不需要修改 ──→ @Prop
│ ├─ 需要修改并同步回父组件 ──→ @Link
│ └─ 对象内部属性需要被观察 ──→ @ObjectLink(配合父组件 @Observed)
│
├─ 向子组件传递数据
│ ├─ 传递基本类型的值 ──→ @Prop
│ ├─ 传递并需要双向绑定 ──→ @Link
│ └─ 传递可观察对象 ──→ @Observed + @ObjectLink
│
├─ 跨层级共享(祖先到后代,跳过中间层)
│ ├─ 写入方 ──→ @Provide
│ └─ 读取方 ──→ @Consume
│
├─ 应用级全局状态
│ ├─ 需要双向绑定(组件可修改) ──→ @StorageLink
│ ├─ 只需要读取 ──→ @StorageProp
│ └─ 环境变量/系统信息 ──→ @StorageProp
│
├─ 监听状态变化
│ └─ @Watch(搭配 @State/@Prop/@Link 等)
│
├─ UI 结构复用
│ ├─ 组件内部的可复用片段 ──→ @Builder
│ └─ 支持外部传入自定义内容 ──→ @BuilderParam
│
└─ 样式复用
├─ 属性级别的样式复用 ──→ @Styles
└─ 组件级别的样式扩展 ──→ @Extend
逐一拆解:每个装饰器的正确用法
@State:组件自己的小秘密
@State 是最基础、用得最多的装饰器。它声明的变量只属于当前组件,外界看不到也改不了。
适用场景:组件内部的 UI 状态——面板是否展开、输入框的当前值、动画的进度等。
@Component
struct ExpandablePanel {
@State isExpanded: boolean = false
@State inputText: string = ''
build() {
Column() {
Row() {
Text('高级选项').fontSize(14).layoutWeight(1)
Text(this.isExpanded ? '收起' : '展开')
.fontSize(12)
.fontColor('#007DFF')
.onClick(() => { this.isExpanded = !this.isExpanded })
}
.width('100%').padding(12)
if (this.isExpanded) {
TextInput({ placeholder: '请输入...', text: this.inputText })
.width('100%')
.onChange((value: string) => { this.inputText = value })
.padding(12)
}
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(8)
}
}


最佳实践:@State 变量应该初始化为一个合理的默认值。框架会用初始值的类型来做类型推断,所以别用 null 或 undefined 做初始值。
常见错误:把本该用 @Prop 接收的数据用 @State 定义了,然后在 aboutToAppear 里从外部赋值。这种写法虽然能跑,但破坏了数据流的清晰性。
@Prop:父传子的单行道
@Prop 用于子组件接收父组件传来的数据。它是值传递——子组件拿到的是拷贝,修改不会影响父组件。
适用场景:纯展示型组件,比如头像、标签、卡片等。
@Component
struct TagView {
@Prop label: string = ''
@Prop color: string = '#007DFF'
@Prop size: 'small' | 'large' = 'small'
build() {
Text(this.label)
.fontSize(this.size === 'small' ? 10 : 13)
.fontColor('#FFFFFF')
.backgroundColor(this.color)
.borderRadius(12)
.padding({
left: this.size === 'small' ? 8 : 12,
right: this.size === 'small' ? 8 : 12,
top: this.size === 'small' ? 2 : 4,
bottom: this.size === 'small' ? 2 : 4
})
}
}
// 使用
TagView({ label: '新品', color: '#FF4D4F', size: 'large' })
TagView({ label: '限时', color: '#FAAD14' })
最佳实践:@Prop 声明的变量一定要有默认值。因为父组件可能忘记传参,默认值是你的安全网。
注意:@Prop 支持基本类型(string、number、boolean)和数组。对象类型也能传,但因为是值拷贝,大对象的拷贝会有性能开销。
@Link:父子之间的双向通道
@Link 也是父传子,但它是引用传递——子组件修改了值,父组件也会同步更新。
适用场景:子组件需要修改数据并通知父组件——比如一个开关组件、一个计数器组件。
@Component
struct CounterComponent {
@Link count: number
@Prop step: number = 1
build() {
Row({ space: 12 }) {
Button('-')
.width(32).height(32)
.fontSize(18)
.onClick(() => { this.count = Math.max(0, this.count - this.step) })
Text(this.count.toString())
.fontSize(20)
.fontWeight(FontWeight.Bold)
.width(50)
.textAlign(TextAlign.Center)
Button('+')
.width(32).height(32)
.fontSize(18)
.onClick(() => { this.count += this.step })
}
}
}
// 父组件中使用
@Entry
@Component
struct CounterPage {
@State itemCount: number = 5
build() {
Column({ space: 16 }) {
Text(`当前数量: ${this.itemCount}`).fontSize(16)
CounterComponent({ count: $itemCount, step: 2 })
// 注意用 $ 符号传递引用
}
.padding(16)
}
}
最佳实践:用 $ 符号来传递 @Link 参数(如 $itemCount),这是 ArkUI 的语法约定。@Link 变量不需要设置默认值——它的值永远来自父组件。
和 @Prop 的选择:如果子组件只展示数据不修改 → @Prop;如果子组件需要修改数据 → @Link。简单粗暴但有效。
@Provide/@Consume:跨层级的数据共享
当数据需要在组件树中"穿透"好几层时,用 @Prop 或 @Link 一层层传太痛苦了。@Provide 和 @Consume 就是解决这个问题的。
适用场景:主题配置、用户信息、语言设置等全局性的、多个层级都需要用到的数据。
// 祖先组件
@Component
struct AppRoot {
@Provide('theme') currentTheme: string = 'light'
@Provide('user') userName: string = '开发者'
build() {
Column() {
HeaderBar()
ContentArea()
}
}
}
// 中间组件(不需要知道 theme 和 userName 的存在)
@Component
struct ContentArea {
build() {
Column() {
ArticleList()
}
}
}
// 深层后代组件,直接使用 @Consume 读取
@Component
struct ArticleCard {
@Consume('theme') currentTheme: string
@Consume('user') userName: string
build() {
Column() {
Text('文章标题')
.fontColor(this.currentTheme === 'dark' ? '#FFFFFF' : '#333333')
Text(`作者: ${this.userName}`)
.fontSize(12)
.fontColor('#999999')
}
.backgroundColor(this.currentTheme === 'dark' ? '#2C2C3A' : '#FFFFFF')
}
}
最佳实践:给 @Provide 和 @Consume 加上字符串别名(如 'theme'),避免依赖变量名匹配导致的脆弱绑定。如果中间某个组件也需要这个值,它可以同时 @Consume 和 @Provide 同一个 key,实现"覆盖"效果。
和 @StorageLink 的区别:@Provide/@Consume 是组件树内的共享,跟随组件的生命周期;@StorageLink 是应用级的,跟随应用的生命周期并且会持久化。
@Observed/@ObjectLink:深层对象观察
这对组合专门解决"对象内部属性变化不触发 UI 更新"的问题。在上一篇文章(任务管理应用)里我们详细讲了这对组合的用法。
适用场景:列表中每个条目是复杂对象、需要在子组件中修改对象属性。
@Observed
class UserInfo {
name: string = ''
age: number = 0
email: string = ''
isOnline: boolean = false
}
@Component
struct UserCard {
@ObjectLink user: UserInfo
build() {
Row() {
Column({ space: 4 }) {
Text(this.user.name).fontSize(16).fontWeight(FontWeight.Bold)
Text(this.user.email).fontSize(12).fontColor('#999999')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
// 点击切换在线状态,@ObjectLink 确保 UI 自动更新
Column()
.width(12).height(12)
.borderRadius(6)
.backgroundColor(this.user.isOnline ? '#52C41A' : '#D9D9D9')
.onClick(() => { this.user.isOnline = !this.user.isOnline })
}
.width('100%')
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(8)
}
}
最佳实践:@Observed 标记的类应该保持结构简洁。@ObjectLink 变量不能在组件内部初始化,必须由父组件传入。如果父组件用 @State 管理 @Observed 对象的数组,添加和删除元素时需要创建新数组来触发列表刷新。
@StorageLink/@StorageProp:应用级持久化
这对组合直接连接 AppStorage,实现应用级别的状态共享和持久化。在设置页面那篇文章里我们用了大量的 @StorageLink。
适用场景:用户设置、主题偏好、登录状态等需要在多个页面间共享且需要持久化的数据。
// 页面A:写入设置
@Component
struct SettingsView {
@StorageLink('app_fontSize') fontSize: number = 16
@StorageLink('app_theme') theme: string = 'light'
build() {
Column() {
Slider({ value: $$this.fontSize, min: 12, max: 24, step: 1 })
// fontSize 变化自动同步到 AppStorage
}
}
}
// 页面B:读取设置(只读)
@Component
struct ContentView {
@StorageProp('app_fontSize') fontSize: number = 16
@StorageProp('app_theme') theme: string = 'light'
build() {
Text('Hello World')
.fontSize(this.fontSize)
.fontColor(this.theme === 'dark' ? '#FFFFFF' : '#333333')
}
}
最佳实践:需要修改全局状态的地方用 @StorageLink,只需要读取的地方用 @StorageProp。避免到处都用 @StorageLink,那样任何一个组件都可能意外修改全局状态,排查问题很头疼。
@Watch:变化监听器
@Watch 不是一个独立的状态装饰器,而是搭配其他装饰器使用的"增强器"。它让你在某个状态变量变化时执行自定义逻辑。
适用场景:表单联动计算、数据校验、变化日志、动画触发等。
@Component
struct FormValidator {
@State @Watch('validateEmail') email: string = ''
@State emailError: string = ''
@State emailValid: boolean = false
validateEmail(propName: string): void {
if (this.email.length === 0) {
this.emailError = ''
this.emailValid = false
return
}
let regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (regex.test(this.email)) {
this.emailError = ''
this.emailValid = true
} else {
this.emailError = '邮箱格式不正确'
this.emailValid = false
}
}
build() {
Column({ space: 4 }) {
TextInput({ placeholder: '请输入邮箱', text: this.email })
.onChange((value: string) => { this.email = value })
.borderColor(this.emailError.length > 0 ? '#FF4D4F' : '#E0E0E0')
if (this.emailError.length > 0) {
Text(this.emailError).fontSize(11).fontColor('#FF4D4F')
}
if (this.emailValid) {
Text('格式正确').fontSize(11).fontColor('#52C41A')
}
}
.alignItems(HorizontalAlign.Start)
}
}
最佳实践:@Watch 回调里避免修改触发它的那个变量(会导致循环)。多个变量需要联动计算时,用一个统一的回调方法。
@Builder/@BuilderParam:UI 结构复用
@Builder 用于封装可复用的 UI 片段,@BuilderParam 用于让组件接受外部传入的 Builder 作为"插槽"。
适用场景:重复出现的 UI 模式(列表项、分隔线、标题栏等),以及需要支持自定义内容的容器组件。
@Builder
function SectionHeader(title: string, subtitle?: string) {
Column({ space: 2 }) {
Text(title).fontSize(16).fontWeight(FontWeight.Bold)
if (subtitle) {
Text(subtitle).fontSize(12).fontColor('#999999')
}
}
.width('100%')
.alignItems(HorizontalAlign.Start)
.padding({ bottom: 8 })
}
// 可接受外部内容的容器组件
@Component
struct CardContainer {
@Prop title: string = ''
@BuilderParam content: () => void = this.defaultContent
@Builder
defaultContent() {
Text('暂无内容').fontSize(14).fontColor('#999999')
}
build() {
Column({ space: 8 }) {
SectionHeader(this.title)
this.content()
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding(16)
}
}
// 使用时传入自定义内容
CardContainer({
title: '今日推荐',
content: () => {
Column({ space: 8 }) {
Text('推荐内容1').fontSize(14)
Text('推荐内容2').fontSize(14)
}
}
})
@Styles/@Extend:样式层面的复用
@Styles 复用一组样式属性,@Extend 在系统组件的样式基础上做扩展。
适用场景:多个组件共享同一套样式(卡片样式、按钮样式、输入框样式等)。
@Styles
function cardStyle() {
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding(16)
.shadow({ radius: 4, color: '#0A000000', offsetX: 0, offsetY: 2 })
}
@Styles
function primaryButtonStyle() {
.backgroundColor('#007DFF')
.borderRadius(8)
.fontColor('#FFFFFF')
.fontSize(14)
.fontWeight(FontWeight.Medium)
}
// @Extend 扩展系统组件
@Extend(Button)
function dangerButton() {
.backgroundColor('#FF4D4F')
.fontColor('#FFFFFF')
.borderRadius(8)
}
// 使用
Column() {
Text('卡片内容').cardStyle()
Button('主要操作').primaryButtonStyle()
Button('危险操作').dangerButton()
}
@Styles 和 @Extend 的区别:@Styles 是通用的样式集合,可以用在任何组件上;@Extend 是针对某个特定系统组件的样式扩展,只能用在对应的组件类型上。
场景化的最佳实践汇总
场景一:电商商品详情页
商品详情页的数据来源多、层级深。典型的状态管理方案:
- 商品信息(名称、价格、图片)→
@Prop传递给各子组件展示 - 购买数量 →
@Link实现计数器和总价组件的双向绑定 - 用户已选规格 →
@Provide让多个 Tab 页共享 - 价格和库存的实时计算 →
@Watch监听规格变化触发重算 - 用户偏好(字号、主题)→
@StorageProp全局读取
场景二:社交应用的消息列表
- 消息数据 →
@Observed标记 Message 类 - 消息卡片组件 →
@ObjectLink接收,支持"已读"状态的修改 - 未读数统计 →
@Provide让头部导航栏和消息列表共享 - 消息气泡的不同样式 →
@Builder封装不同类型的消息 UI - 深色模式 →
@StorageProp读取主题配置
场景三:表单填写页
- 表单各字段 →
@State管理每个输入值 - 字段校验 →
@Watch监听输入变化执行校验逻辑 - 提交按钮状态 → 基于校验结果计算
- 表单模板(标题+输入框+错误提示)→
@Builder复用 - 用户自动填充的历史记录 →
@StorageLink持久化
容易踩的几个坑
坑一:@State 对象属性变化不触发更新
这大概是 HarmonyOS6 PC 开发者被问得最多的问题了。@State 只跟踪变量的引用变化,不跟踪对象内部属性。如果你 this.obj.name = 'new',UI 不会更新。
解决方案:用 @Observed 标记类,子组件用 @ObjectLink 接收。或者手动触发:this.obj = { ...this.obj, name: 'new' }。
坑二:@Link 没加 $ 符号
父组件传参时忘记加 $(比如写成 count: this.count 而不是 count: $count),结果是传了值而不是引用。子组件用 @Link 接收时会报错或者行为异常。
坑三:@Provide 的 key 冲突
如果两个祖先组件都 @Provide 了同名的 key,后代 @Consume 时会拿到哪个?答案是最近的祖先。但这种隐式的覆盖很容易造成 bug。建议用唯一的 key 名,比如 provide_settings_theme。
坑四:数组的不可变更新
直接 this.array.push(item) 不会触发 UI 更新。正确的写法是 this.array = [...this.array, item]。删除也一样:this.array = this.array.filter(...) 而不是 splice。
坑五:@Builder 里访问不到 this
全局的 @Builder 函数(不在组件内定义的)没有 this 上下文。如果你需要在 Builder 里访问组件状态,必须把 Builder 定义在组件内部,或者通过参数传递。
一张总结表
| 装饰器 | 数据流向 | 绑定方式 | 适用粒度 | 持久化 |
|---|---|---|---|---|
| @State | 组件内部 | 双向 | 基本类型/对象 | 否 |
| @Prop | 父→子 | 单向 | 基本类型/数组 | 否 |
| @Link | 父↔子 | 双向 | 基本类型/对象 | 否 |
| @Provide | 祖先→后代 | 双向 | 基本类型/对象 | 否 |
| @Consume | 祖先→后代 | 双向 | 基本类型/对象 | 否 |
| @Observed | - | - | 类 | 否 |
| @ObjectLink | 父→子 | 双向(属性级) | @Observed对象 | 否 |
| @StorageLink | AppStorage↔组件 | 双向 | 基本类型/对象 | 是 |
| @StorageProp | AppStorage→组件 | 单向 | 基本类型/对象 | 是 |
| @Watch | - | 监听 | 搭配其他使用 | - |
| @Builder | - | - | UI片段 | - |
| @BuilderParam | 外部→组件 | 插槽 | UI片段 | - |
| @Styles | - | - | 属性样式 | - |
| @Extend | - | - | 组件样式 | - |
小结
状态管理选型的核心在于搞清楚数据的流向和变化的方式。组件内部用 @State,父传子看是否需要回写来决定 @Prop 或 @Link,对象深层变化用 @Observed/@ObjectLink,跨层级用 @Provide/@Consume,全局持久化用 @StorageLink/@StorageProp。
不需要一口气记住所有装饰器。把这篇文章当个速查手册,遇到具体场景时回来对照决策树,几次之后你就能形成直觉了。HarmonyOS6 PC 的状态管理体系虽然看起来复杂,但每个装饰器都有明确的职责边界。搞清楚了这些边界,写出来的代码自然就干净了。
更多推荐



所有评论(0)