在这里插入图片描述

HarmonyKit | 鸿蒙新特性:@State @Prop @Provide @Consume 状态管理四件套

引言:四个装饰器,一张数据流图

ArkUI 提供了四个核心状态管理装饰器:@State@Prop@Provide@Consume。在 HarmonyKit 的实际使用中,这四个装饰器构成了完整的数据流体系——从页面级的状态管理,到父→子的属性传递,再到跨层级的全局数据共享。

不同于 React 的 useState/useContext/Redux 组合或 Vue 的 data/props/provide-inject 体系,ArkUI 将这四种能力统一在装饰器机制下。理解它们之间的差异和适用场景,是写好 ArkUI 应用的基本功。

项目仓库:https://atomgit.com/VON-/harmony-kit

@State:组件拥有的可变状态

@State 是使用频率最高的装饰器。当它修饰的变量值变化时,组件的 build() 方法重新执行,UI 自动刷新。

HarmonyKit 中每个工具页都有 3-6 个 @State 变量:

@State input: string = '255';         // 用户输入
@State fromRadix: number = 10;          // 当前进制选择
@State results: string[] = ['', '', '', ''];  // 4 种进制结果

@State 的两个关键规则:

  • 必须是简单类型或可观察类型:string、number、boolean 是安全的。数组和 class 也是可观察的——但需要注意,数组的 push 操作不会被观察到,需要用 this.arr = [...this.arr, newItem] 的方式触发刷新。
  • 只能在组件内部修改:不能从外部直接赋值。父组件不能写 child.stateVar = x——这打破了封装性。

HarmonyKit 中的典型 @State 模式:

// 进制转换页面 — 用户输入变化 → 重新计算结果
@State input: string = '255';

onInputChange(value: string) {
  this.input = value;
  this.recalculate();  // 更新 results 数组
}

build() {
  Column() {
    TextInput({ text: this.input, placeholder: '输入数字' })
      .onChange((value: string) => this.onInputChange(value))
    // results 随 input 变化自动更新
    ForEach(this.results, (item: RadixResult) => { ... })
  }
}

数据的流动是单向的:用户输入 → onChange 回调 → 更新 @State → build() 重渲染。没有双向绑定的隐式数据流,修改的可追溯性非常清晰。

@State 的深度理解:可变引用 vs 不可变替换

对于数组和对象类型的 @State 变量,有一个常见的陷阱:

// 错误:数组原地修改不会触发 UI 刷新
@State items: string[] = ['a'];
this.items.push('b');  // 数组变了,但 UI 不会更新

// 正确:通过重新赋值触发刷新
@State items: string[] = ['a'];
this.items = [...this.items, 'b'];  // 新数组,UI 更新

这个行为的根源是:@State 通过引用比较来判断是否需要刷新。push 修改了数组的内部内容,但没有改变数组的引用——@State 认为"值没有变"。用扩展运算符创建新数组会产生新引用,@State 检测到变化,触发渲染。

在实际开发中,如果在 ForEach 中操作数组,这个区别尤为重要:

// ForEach 回调中的数组操作
ForEach(this.results, (item: string, index: number) => {
  Button('删除')
    .onClick(() => {
      // 错误:this.results.splice(index, 1) → UI 不更新
      // 正确:
      this.results = this.results.filter((_: string, i: number) => i !== index);
    })
})

@Prop:父传子的只读属性

@Component
export struct ToolCard {
  @Prop tool: ToolItem;  // 父组件传入,子组件只读
}

@Prop 是从父组件接收数据的通道。子组件不拥有数据——只是展示它。父组件(Index.ets)拥有数据(TOOL_LIST),子组件(ToolCard)接收并渲染。单向数据流保证了数据修改的可追溯性——如果你想知道 tool 的数据从哪里来,永远往父组件方向查找。

在 HarmonyKit 中,10 个工具的 ToolCard 都接收相同的 @Prop tool,但各自展示不同的内容。因为传入的 ToolItem 不同(颜色、图标、名称不同),渲染结果自然不同。

@Prop 的一个重要特性是:子组件内部无法修改 @Prop 的值。 如果需要修改,必须通过父组件来操作。这是一种"数据向下,事件向上"的模式:

// 子组件
@Component
export struct ToolCard {
  @Prop tool: ToolItem;
  onClick?: () => void;  // 回调函数由父组件传入

  build() {
    Column()
      .onClick(() => {
        this.onClick?.();  // 通知父组件处理点击事件
      })
  }
}

// 父组件
ToolCard({
  tool: item,
  onClick: () => {
    router.pushUrl({ url: item.route });  // 父组件处理导航
  }
})

@Provide/@Consume:跨层级共享

// Index.ets — 数据的"水源"
@Provide('pathStack') pathStack: NavPathStack = new NavPathStack();

// ToolCard.ets — 数据的"消费者"
@Consume('pathStack') pathStack: NavPathStack;

这套装饰器让祖先组件可以向所有后代组件注入数据,无需通过中间的组件层层传递。在 React 中实现同样效果需要 Context + Provider + useContext 三件套。

HarmonyKit 只用了一组 Provide/Consume——用 'pathStack' 这个 key 来共享 NavPathStack 实例。所有需要访问导航栈的组件(ToolCard、工具页面)通过 @Consume 获取同一实例。

当 @Provide 的变量变化时,所有 @Consume 该变量的后代组件自动重渲染。不需要手动通知。

@Provide/@Consume 的工作机制有几个重要细节:

  1. 字符串 key 匹配:@Provide 和 @Consume 通过相同的 key 匹配。key 是字符串,在同一个组件树内唯一即可。
  2. 就近原则:如果多个祖先都 @Provide 了同一个 key,后代 @Consume 匹配最近的那个祖先。
  3. 可观察性:@Provide 的变量变化时,所有 @Consume 该变量的后代组件自动刷新。
// 另一个提供/消费的例子:主题色共享
// 父组件
@Provide('themeColor') themeColor: string = '#007AFF';

// 深层子组件
@Consume('themeColor') themeColor: string;
// 可以直接用 themeColor 设置样式,无需逐层传递

@Watch:响应式监听

与 @State 搭配使用的还有一个 @Watch 装饰器——当 @State 变量变化时,自动调用被 @Watch 装饰的方法:

@State('input') @Watch('onInputChanged') input: string = '';

onInputChanged() {
  // input 变化后自动执行
  this.recalculate();
}

@Watch 让"数据变化 → 自动处理"的模式变得更加简洁。如果在进制转换工具中,用户输入变化时需要自动重新计算所有进制结果,用 @Watch 可以直接在 input 变化时触发计算,而不需要在每个 onChange 回调中手动调用。

@Link:双向绑定

虽然 HarmonyKit 没有使用 @Link,但它值得一提。@Link 建立父组件和子组件之间的双向绑定:

// 父组件
@State count: number = 0;
Child({ count: $count })

// 子组件
@Link count: number;
// 子组件可以修改 count,父组件的值也会同步变化

@Link 通过 $ 语法传递变量的引用。在需要父子组件共享同一状态并允许双方修改的场景中(如表单编辑器的数据同步),@Link 比 @Prop + 事件回调的组合更简洁。

为什么不使用 AppStorage/LocalStorage

ArkUI 还提供了 AppStorage(应用级全局状态)和 LocalStorage(页面级全局状态)。HarmonyKit 没有使用它们,原因如下:

  • AppStorage 的全局作用域可能导致"状态爆炸"——任何组件都可以读写任何状态。这与 HarmonyKit 追求的"最小状态、显式数据流"原则相悖。
  • LocalStorage 适用于 @Entry 页面间的状态共享。但 HarmonyKit 的页面间状态共享需求极少(每个工具页是独立的,除了导航栈不需要共享其他状态)。

"够用就好"是 HarmonyKit 的状态管理哲学。10 个工具页面的复杂度不需要 Redux 级别的状态管理方案。

选择决策树

数据只在当前组件内变化 → @State
父组件传数据给子组件,子组件只读 → @Prop
父子组件需要共享同一状态并允许双方修改 → @Link
跨多个层级共享数据 → @Provide + @Consume
应用级全局状态持久化 → AppStorage
页面级全局状态 → LocalStorage
需要监听状态变化执行逻辑 → @State + @Watch

状态管理的常见陷阱

  1. 忘记 @State:定义的变量没有加 @State,修改后 UI 不刷新。后果:数据变了,页面还是旧的。
  2. 数组原地修改:用 push/splice 修改 @State 数组,UI 不刷新。解决方案:总是创建新数组赋值。
  3. 过度使用 @Provide:所有数据都用 @Provide 共享,导致数据流不清晰。解决方案:限制 @Provide 的使用范围,优先用 @State 和 @Prop。
  4. 解构 @State 变量let { a, b } = this 会导致 @State 失去响应性。解决方案:通过 this.athis.b 直接访问。

反模式:过度的全局状态

HarmonyKit 没有使用任何全局状态管理方案(Redux、Vuex、Pinia、AppStorage)。不是因为"工具太简单所以不需要"——而是因为 @Provide/@Consume 恰好覆盖了 HarmonyKit 唯一的跨层级数据需求(导航栈)。

引入全局状态管理库的代价是显著的——额外的概念(action/mutation/reducer)、额外的样板代码、额外的调试工具。对于 10 个工具页的复杂度,@Provide/@Consume 是"刚好够用"的解决方案。

“如果你有一个锤子,所有问题看起来都像钉子。”——在状态管理上的克制,和专业能力的选择同样重要。

项目仓库:https://atomgit.com/VON-/harmony-kit

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐