鸿蒙 HarmonyOS 6|ArkUI(03):状态管理
前言
我们这篇文章会把 ArkUI 在鸿蒙 6 里的状态管理讲清楚。
我们会按照三个层次来拆,也就是组件内局部状态、父子之间的同步、跨层的共享。我们会配上可运行的最小片段,并在最后给出一张决策表和一份常见误用清单。
一、先把三个层次的全景图说清楚
在 HarmonyOS 6 的 ArkUI 里,状态管理有一套清晰的装饰器体系。组件自己的可变数据用 @State 来承载,父子之间的单向同步用 @Prop 来承载,父子之间需要双向同步时用 @Link 来承载,跨多层的共享用 @Provide 和 @Consume 来承载。
二、组件内局部状态:@State
@State 表示这个组件的本地可变数据。我们只要更新它,ArkUI 就会定位到依赖它的节点并触发重渲染。这个特性让我们可以放心把组件的细小交互放在本地管理,不必把所有变量都提到共享层。
@Component
export struct CounterCard {@State count: number = 0;build() {Column({ space: 10 }) {Text(`当前计数是 ${this.count}`).fontSize(16)Button('加一').onClick(() => {this.count += 1;})}.padding(12)}
}@Entry
@Component
struct Index {build() {Column({ space: 16 }) {CounterCard()}.justifyContent(FlexAlign.Center) // 让组件在竖直方向居中.width('100%').height('100%').padding(24)}
}

这段代码体现了状态驱动的基本套路。我们把界面写成状态的投影,用户点一次按钮,count 改一次,界面就会跟着更新。官方的状态章节明确说明了这种渲染绑定的机制和适用范围,我们保持就近封装就可以。
三、父子单向同步:@Prop
当父组件有一个数据需要交给子组件展示,但不希望子组件反向修改父组件时,我们使用 @Prop。父组件的状态变化会单向同步到子组件,而子组件对 @Prop 的修改不会再写回父组件的数据源。
@Component
export struct TitleView {@Prop title: stringbuild() {Text(this.title).fontSize(18).fontWeight(FontWeight.Medium)}
}@Component
export struct PageHeader {@State pageTitle: string = '我的页面'build() {Column({ space: 8 }) {TitleView({ title: this.pageTitle })Button('改标题').onClick(() => {this.pageTitle = '新的标题'})}.padding(12)}
}@Entry
@Component
struct Index {build() {Column({ space: 16 }) {PageHeader()}.justifyContent(FlexAlign.Center).width('100%').height('100%').padding(24)}
}

在这段代码里,PageHeader 持有真实状态,TitleView 只接收并展示。官方的 @Prop 文档还强调了初始化规则和与其他装饰器的组合关系,我们按文档理解数据流,就能避免意外的双向写回。
四、父子双向同步:@Link
当子组件需要既接到父组件的值,又要把自己的修改同步回父组件时,我们使用 @Link。这相当于在父子之间建立一条双向的通道,父变子跟,子改父也变,非常适合输入框这类场景。(Medium)
@Component
export struct NameInput {@Link name: stringbuild() {Column({ space: 8 }) {Text('请输入名字').fontSize(14)TextInput({ text: this.name }).onChange((v: string) => {this.name = v}).width('100%')}.padding(12)}
}@Component
export struct ProfileForm {@State userName: string = '小雨'build() {Column({ space: 10 }) {// 关键:@Link 需要按“引用”传参NameInput({ name: $userName })Text(`欢迎你,${this.userName}`).fontSize(16)}.padding(12)}
}@Entry
@Component
struct Index {build() {Column({ space: 16 }) {ProfileForm()}.justifyContent(FlexAlign.Center).width('100%').height('100%').padding(24)}
}

这里的 NameInput 收到初始值后,可以把输入变化回写到 ProfileForm。
五、跨层共享:@Provide 和 @Consume
当多个层级的组件都需要访问同一份数据,而中间层只是路过时,我们使用 @Provide 和 @Consume。父辈通过 @Provide 暴露一个可共享的状态,后代通过 @Consume 直接拿到这份状态,并且形成双向同步的关系。
// 顶层提供共享状态
@Component
export struct AppShell {@Provide theme: 'light' | 'dark' = 'light'build() {Column({ space: 12 }) {ThemeSwitcher()ContentArea()}.padding(12)}
}// 任意后代直接消费共享状态
@Component
export struct ThemeSwitcher {@Consume theme: 'light' | 'dark'build() {Row({ space: 8 }) {Text(`当前主题是 ${this.theme}`)Button('切到浅色').onClick(() => this.theme = 'light')Button('切到深色').onClick(() => this.theme = 'dark')}}
}
这段代码说明了共享的真正含义。我们在顶层放一次 @Provide,就能让深层组件通过 @Consume 直接拿到同一份变量。官方文档明确指出两者之间是双向同步关系,所以后代的修改也会反馈到提供者的变量上,我们写共享逻辑时要有这个意识。
六、把状态和导航配合起来更顺手
在鸿蒙 6 的推荐导航范式里,我们通常会在入口页通过 @Provide 暴露一个 NavPathStack,让子页在需要返回时直接 pop,或者在需要进入下一个子页时 pushPath。这种写法把页面状态和页面栈都放在一个可控的范围里,跳转和返回更容易理解。
@Entry
@Component
export struct Index {@Provide('stack') stack: NavPathStack = new NavPathStack()@Builder pageMap(name: string) {if (name === 'Detail') {DetailPage()}}build() {Navigation(this.stack) {Button('进入详情').onClick(() => this.stack.pushPath({ name: 'Detail' }))}.navDestination(this.pageMap)}
}@Component
export struct DetailPage {@Consume('stack') stack: NavPathStackbuild() {NavDestination() {Button('返回').onClick(() => this.stack.pop())}}
}
我们把页面栈作为共享状态提供给后代,这样子页就不需要额外拿路由句柄了。官方的导航文档和相关问答都建议用这条思路来组织页面,代码更简洁,也更符合鸿蒙 6 的范式。(华为开发者官方网站)
七、选型决策表:先局部,后父子,再跨层
我们现在把日常开发里的常见情景放进一张表,方便快速决策。
| 场景 | 推荐装饰器 | 数据方向 | 说明 |
|---|---|---|---|
| 组件内部的小交互 | @State | 组件内单点更新 | 变更只影响本组件,渲染范围最小,成本最低。 |
| 父传子展示 | @Prop | 父到子单向 | 子组件不回写,稳定可控,利于定位问题。 |
| 父子联动输入 | @Link | 父子双向 | 仅在确需回写时使用,避免到处双向同步。 |
| 多层共享主题或会话 | @Provide/@Consume | 祖先与后代双向 | 避免层层传参,控制好共享的边界与变更频率。 |
这张表背后的原则很直白。共享范围越小,优先级越高;能局部就局部,能单向就不双向,能父子就不要全局。等到确实需要跨层共享时,我们再引入 @Provide/@Consume,并把更新频率和依赖范围都控制住。
八、最佳实践清单
第一条建议是控制更新面。我们尽量在计算阶段使用临时变量,把最终结果一次性写回状态变量,这样可以减少多次写状态带来的重复渲染。
第二条建议是分层管理状态。我们把组件内状态与共享状态分开,避免把所有变量都提升为共享,这样渲染边界会更清楚。
第三条建议是谨慎使用双向同步。我们在能用单向数据流完成需求的情况下,就不要轻易引入 @Link,因为它会扩大可变范围。只有在输入控件确实需要回写到父组件时,才使用双向。
第四条建议是把跨层共享和导航解耦。我们让 @Provide/@Consume 只处理需要跨层的状态,让 Navigation 只处理页面栈,这样两个系统各司其职,问题更容易定位。
九、总结
我们已经把 HarmonyOS6 的 ArkUI 状态管理拆成了三个层次,并且用示例把每个层次都跑了起来。
