HarmonyOS6踩坑记录之卡片开发 @Prop 和 @Link 搞混了?3 个坑帮你彻底搞懂父子组件传值
文章目录
- 场景还原:一个天气卡片引发的血案
- 坑 1:@Prop 改了子组件,父组件纹丝不动
- 坑 2:换 @Link 后 aboutToAppear 直接编译报错
- 坑 3:@Link 改一个字段,所有子组件全炸了
- 终极方案:@ObservedV2 + @Trace,精确到字段的状态管理
- 三种方案怎么选?
- 写在最后
上周做一个天气卡片,被@Prop和@Link折腾了整整一下午。子组件改了温度父组件不同步、换成@Link编译直接报错、好不容易跑通了其他子组件又跟着乱变。说实话,鸿蒙的状态管理装饰器要是没搞清底层机制,真的会反复踩坑。
今天把这段经历整理出来,顺便讲清楚什么时候该用@ObservedV2+@Trace。
场景还原:一个天气卡片引发的血案
需求很简单。父组件从服务端拉天气数据,传给子组件展示。子组件有个城市下拉框,选了新城市要更新卡片内容。
父组件大概长这样:
@Componentstruct WeatherCard{@StateweatherData:WeatherInfo={city:'北京',temp:25,condition:'晴',humidity:40}build(){Column(){WeatherDetail({data:this.weatherData})WeatherTrend({data:this.weatherData})}}}看起来没问题对吧?坑在后面。
坑 1:@Prop 改了子组件,父组件纹丝不动
最开始我用@Prop接收数据:
@Componentstruct WeatherDetail{@Propdata:WeatherInfobuild(){Column(){Text(`${this.data.city}${this.data.temp}°C`)Button('升温').onClick(()=>{this.data.temp=30// 只改了本地副本,父组件不受影响})}}}点击按钮,界面上的温度确实变成了 30,但父组件的weatherData.temp还是 25。
原因很直接:@Prop是单向绑定,传给子组件的是值的拷贝。子组件改的是自己的副本,跟父组件没有任何关系。
如果你的场景就是"父传子展示,子组件不需要回写",那@Prop完全够用。但只要子组件需要把改动同步回父组件,@Prop就搞不定了。
坑 2:换 @Link 后 aboutToAppear 直接编译报错
想着@Prop不行那就换@Link呗。结果一跑:
@Componentstruct WeatherDetail{@Linkdata:WeatherInfoaboutToAppear(){this.data=fetchLatestWeather()// 编译报错!}}报错信息大意是@Link装饰的变量不能在子组件中初始化。
当时挺懵的。后来才搞明白:@Link要求变量必须由父组件注入,子组件不能自己初始化。框架看到@Link就会去找父组件的$$绑定来赋值,你在aboutToAppear里赋值会和框架注入冲突,编译器直接不给过。
正确姿势是不初始化:
@Componentstruct WeatherDetail{@Linkdata:WeatherInfo// 不要赋值,让父组件传aboutToAppear(){// 可以读 this.data,但不要整体赋值console.info(`当前城市:${this.data.city}`)}}父组件那边要用$$语法:
WeatherDetail({data:$$this.weatherData})坑 3:@Link 改一个字段,所有子组件全炸了
编译过了之后又出了个更离谱的问题——在WeatherDetail里切了城市改了temp,旁边的WeatherTrend组件也跟着重新渲染了。
// 子组件改了 tempthis.data.temp=30// 结果父组件的 weatherData 整个变了// 另一个子组件 WeatherTrend 也收到更新,疯狂重绘原因也不复杂:@Link传的是对象引用,不是拷贝。子组件改this.data.temp,改的就是父组件那个对象。父组件的@State weatherData被整体标记为"已变更",所有绑定了weatherData的子组件全部触发重渲染。
说白了,@Link虽然实现了双向绑定,但粒度太粗。你改一个字段,框架认为整个对象变了。
终极方案:@ObservedV2 + @Trace,精确到字段的状态管理
踩完三个坑之后,终于找到了正经的解决方案——V2 状态管理。
@ObservedV2标记类,@Trace标记需要追踪的字段。每个字段独立追踪变更,改temp不会影响绑定了city的组件。
先定义数据模型:
@ObservedV2classWeatherInfo{@Tracecity:string='北京'@Tracetemp:number=25@Tracecondition:string='晴'@Tracehumidity:number=40}子组件用@Local接收:
@Componentstruct WeatherDetail{@Localdata:WeatherInfobuild(){Column(){Text(`${this.data.city}${this.data.temp}°C${this.data.condition}`)Button('切换城市').onClick(()=>{this.data.city='上海'// 只触发绑定了 city 的组件更新})}}}父组件还是用@State:
@Componentstruct WeatherCard{@StateweatherData:WeatherInfo=newWeatherInfo()build(){Column(){WeatherDetail({data:this.weatherData})WeatherTrend({data:this.weatherData})}}}这样改城市只会触发显示城市名的组件更新,温度组件不受影响。改温度也不会触发趋势图重绘。每个@Trace字段都是独立的更新单元。
三种方案怎么选?
说到底就是一个决策问题:
@Prop:子组件只读、不回写。数据是基本类型或者你只需要展示,用这个最省心。
@Link:需要双向绑定,但对象结构简单、不怕整体更新。比如只有一个子组件用这个数据,或者重渲染代价不大。
@ObservedV2+@Trace:复杂对象、多个子组件共享数据、需要精确控制更新范围。做卡片开发、多组件联动的场景,直接用这个就对了。
坦白讲,如果你一开始就用 V2 方案,上面三个坑一个都不会踩。但话说回来,不踩这些坑还真不容易理解它们背后的设计逻辑。
写在最后
鸿蒙的状态管理装饰器看着简单,坑其实都藏在细节里。@Prop和@Link是 V1 时代的产物,设计上确实有一些粗糙的地方。V2 的@ObservedV2+@Trace明显更成熟,建议新项目直接上 V2。
如果你还在纠结这几个装饰器怎么选,记住一句话:默认用@ObservedV2+@Trace,除非你的场景足够简单到@Prop就能搞定。
