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

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 的工作机制有几个重要细节:
- 字符串 key 匹配:@Provide 和 @Consume 通过相同的 key 匹配。key 是字符串,在同一个组件树内唯一即可。
- 就近原则:如果多个祖先都 @Provide 了同一个 key,后代 @Consume 匹配最近的那个祖先。
- 可观察性:@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
状态管理的常见陷阱
- 忘记 @State:定义的变量没有加 @State,修改后 UI 不刷新。后果:数据变了,页面还是旧的。
- 数组原地修改:用 push/splice 修改 @State 数组,UI 不刷新。解决方案:总是创建新数组赋值。
- 过度使用 @Provide:所有数据都用 @Provide 共享,导致数据流不清晰。解决方案:限制 @Provide 的使用范围,优先用 @State 和 @Prop。
- 解构 @State 变量:
let { a, b } = this会导致 @State 失去响应性。解决方案:通过this.a和this.b直接访问。
反模式:过度的全局状态
HarmonyKit 没有使用任何全局状态管理方案(Redux、Vuex、Pinia、AppStorage)。不是因为"工具太简单所以不需要"——而是因为 @Provide/@Consume 恰好覆盖了 HarmonyKit 唯一的跨层级数据需求(导航栈)。
引入全局状态管理库的代价是显著的——额外的概念(action/mutation/reducer)、额外的样板代码、额外的调试工具。对于 10 个工具页的复杂度,@Provide/@Consume 是"刚好够用"的解决方案。
“如果你有一个锤子,所有问题看起来都像钉子。”——在状态管理上的克制,和专业能力的选择同样重要。
更多推荐




所有评论(0)