前言
我們這篇文章會把 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: 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)
@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 直接拿到這份狀態,並且形成雙向同步的關係。
// 頂層提供共享狀態
@Component
export struct AppShell {
@Provide theme: 'light' | 'dark' = 'light';
build() {
Column({ space: 12 }) {
ThemeSwitcher()
ContentArea()
}
.justifyContent(FlexAlign.Center)
.padding(24)
.width('100%')
.height('100%')
.backgroundColor(this.theme === 'dark' ? '#0f0f0f' : '#ffffff')
}
}
// 任意後代直接消費共享狀態
@Component
export struct ThemeSwitcher {
@Consume theme: 'light' | 'dark';
build() {
Row({ space: 12 }) {
Text(`當前主題是 ${this.theme}`).fontSize(16)
Button('切到淺色').onClick(() => { this.theme = 'light' })
Button('切到深色').onClick(() => { this.theme = 'dark' })
Button('切換').onClick(() => {
this.theme = this.theme === 'dark' ? 'light' : 'dark'
})
}
}
}
// 內容區域:隨主題變化
@Component
export struct ContentArea {
@Consume theme: 'light' | 'dark';
build() {
Column({ space: 8 }) {
Text(this.theme === 'dark' ? '暗色模式已啓用' : '淺色模式已啓用')
.fontSize(18)
.fontWeight(FontWeight.Medium)
Text('這是一段示例內容').opacity(0.8)
}
.padding(16)
.width('100%')
.backgroundColor(this.theme === 'dark' ? '#1e1e1e' : '#f6f6f6')
.borderRadius(12)
}
}
// 入口頁:根節點必須是“容器”
@Entry
@Component
struct Index {
build() {
Column() {
AppShell() // 自定義組件仍然作為容器的子節點
}
.width('100%')
.height('100%')
}
}
這段代碼説明了共享的真正含義。我們在頂層放一次 @Provide,就能讓深層組件通過 @Consume 直接拿到同一份變量。
六、把狀態和導航配合起來更順手
在鴻蒙 6 的推薦導航範式裏,我們通常會在入口頁通過 @Provide 暴露一個 NavPathStack,讓子頁在需要返回時直接 pop,或者在需要進入下一個子頁時 pushPath。這種寫法把頁面狀態和頁面棧都放在一個可控的範圍裏,跳轉和返回更容易理解。
// 明確聲明允許的路由名
type RouteName = 'Detail'
// 如果後續要傳參,在這裏把字段寫清楚:比如 { id: number; title?: string }
interface DetailParam {}
// 入口:用 Navigation 作為根容器
@Entry
@Component
export struct Index {
@Provide('stack') stack: NavPathStack = new NavPathStack()
// navDestination 的 builder:顯式類型避免 any/unknown
@Builder
pageMap(name: RouteName, param?: DetailParam) {
if (name === 'Detail') {
NavDestination() {
DetailPage()
}
}
}
build() {
Navigation(this.stack) {
Column({ space: 12 }) {
Button('進入詳情')
.onClick(() => {
// 不傳參也符合 DetailParam
this.stack.pushPath({ name: 'Detail' })
})
}
.padding(24)
}
.navDestination(this.pageMap)
}
}
@Component
export struct DetailPage {
@Consume('stack') stack: NavPathStack
build() {
Column({ space: 12 }) {
Text('Detail Page').fontSize(18).fontWeight(FontWeight.Medium)
Button('返回').onClick(() => this.stack.pop())
}
.padding(24)
}
}
我們把頁面棧作為共享狀態提供給後代,這樣子頁就不需要額外拿路由句柄了。官方的導航文檔和相關問答都建議用這條思路來組織頁面,代碼更簡潔,也更符合鴻蒙 6 的範式。
七、選型決策表:先局部,後父子,再跨層
我們現在把日常開發裏的常見情景放進一張表,方便快速決策。
| 場景 | 推薦裝飾器 | 數據方向 | 説明 |
|---|---|---|---|
| 組件內部的小交互 | @State | 組件內單點更新 | 變更隻影響本組件,渲染範圍最小,成本最低。 |
| 父傳子展示 | @Prop | 父到子單向 | 子組件不回寫,穩定可控,利於定位問題。 |
| 父子聯動輸入 | @Link | 父子雙向 | 僅在確需回寫時使用,避免到處雙向同步。 |
| 多層共享主題或會話 | @Provide/@Consume | 祖先與後代雙向 | 避免層層傳參,控制好共享的邊界與變更頻率。 |
這張表背後的原則很直白。共享範圍越小,優先級越高;能局部就局部,能單向就不雙向,能父子就不要全局。等到確實需要跨層共享時,我們再引入 @Provide/@Consume,並把更新頻率和依賴範圍都控制住。
八、最佳實踐清單
第一條建議是控制更新面。我們儘量在計算階段使用臨時變量,把最終結果一次性寫回狀態變量,這樣可以減少多次寫狀態帶來的重複渲染。
第二條建議是分層管理狀態。我們把組件內狀態與共享狀態分開,避免把所有變量都提升為共享,這樣渲染邊界會更清楚。
第三條建議是謹慎使用雙向同步。我們在能用單向數據流完成需求的情況下,就不要輕易引入 @Link,因為它會擴大可變範圍。只有在輸入控件確實需要回寫到父組件時,才使用雙向。
第四條建議是把跨層共享和導航解耦。我們讓 @Provide/@Consume 只處理需要跨層的狀態,讓 Navigation 只處理頁面棧,這樣兩個系統各司其職,問題更容易定位。
九、總結
我們已經把 HarmonyOS6 的 ArkUI 狀態管理拆成了三個層次,並且用示例把每個層次都跑了起來。