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-kitState组件拥有的可变状态State是使用频率最高的装饰器。当它修饰的变量值变化时组件的build()方法重新执行UI 自动刷新。HarmonyKit 中每个工具页都有 3-6 个 State 变量Stateinput:string255;// 用户输入StatefromRadix:number10;// 当前进制选择Stateresults:string[][,,,];// 4 种进制结果State的两个关键规则必须是简单类型或可观察类型string、number、boolean 是安全的。数组和 class 也是可观察的——但需要注意数组的 push 操作不会被观察到需要用this.arr [...this.arr, newItem]的方式触发刷新。只能在组件内部修改不能从外部直接赋值。父组件不能写child.stateVar x——这打破了封装性。HarmonyKit 中的典型 State 模式// 进制转换页面 — 用户输入变化 → 重新计算结果Stateinput:string255;onInputChange(value:string){this.inputvalue;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 刷新Stateitems:string[][a];this.items.push(b);// 数组变了但 UI 不会更新// 正确通过重新赋值触发刷新Stateitems: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.resultsthis.results.filter((_:string,i:number)i!index);})})Prop父传子的只读属性Componentexportstruct ToolCard{Proptool:ToolItem;// 父组件传入子组件只读}Prop是从父组件接收数据的通道。子组件不拥有数据——只是展示它。父组件Index.ets拥有数据TOOL_LIST子组件ToolCard接收并渲染。单向数据流保证了数据修改的可追溯性——如果你想知道 tool 的数据从哪里来永远往父组件方向查找。在 HarmonyKit 中10 个工具的 ToolCard 都接收相同的 Prop tool但各自展示不同的内容。因为传入的 ToolItem 不同颜色、图标、名称不同渲染结果自然不同。Prop 的一个重要特性是子组件内部无法修改 Prop 的值。如果需要修改必须通过父组件来操作。这是一种数据向下事件向上的模式// 子组件Componentexportstruct ToolCard{Proptool:ToolItem;onClick?:()void;// 回调函数由父组件传入build(){Column().onClick((){this.onClick?.();// 通知父组件处理点击事件})}}// 父组件ToolCard({tool:item,onClick:(){router.pushUrl({url:item.route});// 父组件处理导航}})Provide/Consume跨层级共享// Index.ets — 数据的水源Provide(pathStack)pathStack:NavPathStacknewNavPathStack();// 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 建立父组件和子组件之间的双向绑定// 父组件Statecount:number0;Child({count:$count})// 子组件Linkcount:number;// 子组件可以修改 count父组件的值也会同步变化Link 通过$语法传递变量的引用。在需要父子组件共享同一状态并允许双方修改的场景中如表单编辑器的数据同步Link 比 Prop 事件回调的组合更简洁。为什么不使用 AppStorage/LocalStorageArkUI 还提供了 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 是刚好够用的解决方案。“如果你有一个锤子所有问题看起来都像钉子。”——在状态管理上的克制和专业能力的选择同样重要。项目仓库https://atomgit.com/VON-/harmony-kit