前言
我们这篇文章会把 ArkUI 在鸿蒙 6 里的状态管理讲清楚。
我们会按照三个层次来拆,也就是组件内局部状态、父子之间的同步、跨层的共享。我们会配上可运行的最小片段,并在最后给出一张决策表和一份常见误用清单。
一、先把三个层次的全景图说清楚
在 HarmonyOS 6 的 ArkUI 里,状态管理有一套清晰的装饰器体系。组件自己的可变数据用 @State 来承载,父子之间的单向同步用 @Prop 来承载,父子之间需要双向同步时用 @Link 来承载,跨多层的共享用 @Provide 和 @Consume 来承载。
二、组件内局部状态:@State
@State 表示这个组件的本地可变数据。我们只要更新它,ArkUI 就会定位到依赖它的节点并触发重渲染。这个特性让我们可以放心把组件的细小交互放在本地管理,不必把所有变量都提到共享层。
TypeScript
@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 的修改不会再写回父组件的数据源。
TypeScript
@Component
export struct TitleView {
@Prop title: string
build() {
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)
TypeScript
@Component
export struct NameInput {
@Link name: string
build() {
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 直接拿到这份状态,并且形成双向同步的关系。
TypeScript
// 顶层提供共享状态
@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。这种写法把页面状态和页面栈都放在一个可控的范围里,跳转和返回更容易理解。
TypeScript
@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: NavPathStack
build() {
NavDestination() {
Button('返回').onClick(() => this.stack.pop())
}
}
}
我们把页面栈作为共享状态提供给后代,这样子页就不需要额外拿路由句柄了。官方的导航文档和相关问答都建议用这条思路来组织页面,代码更简洁,也更符合鸿蒙 6 的范式。(华为开发者官方网站)
七、选型决策表:先局部,后父子,再跨层
我们现在把日常开发里的常见情景放进一张表,方便快速决策。
| 场景 | 推荐装饰器 | 数据方向 | 说明 |
|---|---|---|---|
| 组件内部的小交互 | @State | 组件内单点更新 | 变更只影响本组件,渲染范围最小,成本最低。 |
| 父传子展示 | @Prop | 父到子单向 | 子组件不回写,稳定可控,利于定位问题。 |
| 父子联动输入 | @Link | 父子双向 | 仅在确需回写时使用,避免到处双向同步。 |
| 多层共享主题或会话 | @Provide/@Consume | 祖先与后代双向 | 避免层层传参,控制好共享的边界与变更频率。 |
这张表背后的原则很直白。共享范围越小,优先级越高;能局部就局部,能单向就不双向,能父子就不要全局。等到确实需要跨层共享时,我们再引入 @Provide/@Consume,并把更新频率和依赖范围都控制住。
八、最佳实践清单
第一条建议是控制更新面。我们尽量在计算阶段使用临时变量,把最终结果一次性写回状态变量,这样可以减少多次写状态带来的重复渲染。
第二条建议是分层管理状态。我们把组件内状态与共享状态分开,避免把所有变量都提升为共享,这样渲染边界会更清楚。
第三条建议是谨慎使用双向同步。我们在能用单向数据流完成需求的情况下,就不要轻易引入 @Link,因为它会扩大可变范围。只有在输入控件确实需要回写到父组件时,才使用双向。
第四条建议是把跨层共享和导航解耦。我们让 @Provide/@Consume 只处理需要跨层的状态,让 Navigation 只处理页面栈,这样两个系统各司其职,问题更容易定位。
九、总结
我们已经把 HarmonyOS6 的 ArkUI 状态管理拆成了三个层次,并且用示例把每个层次都跑了起来。