《级联年度选择》三、Observed与ObjectLink使用指南
HarmonyOS ArkTS @Observed 与 @ObjectLink 装饰器:嵌套类对象属性变化使用指南
本文深入讲解 ArkUI 状态管理中
@Observed和@ObjectLink装饰器的核心原理、使用方法及常见陷阱。通过实际案例帮助你理解嵌套对象属性变化的监听机制,避免状态不刷新的"坑"。
效果
一、问题背景:@State 的观测局限
1.1 @State 能观测什么
@State装饰器可以监听基本数据类型(string、number、boolean)的值变化,以及对象/数组的第一层属性的赋值操作。
@Statecount:number=0// ✅ 值变化可观测@Statename:string=''// ✅ 值变化可观测@Stateuser:User=newUser()// ⚠️ 仅第一层赋值可观测1.2 典型翻车场景
@ObservedclassUserInfo{name:string='张三'age:number=25}@Entry@Componentstruct ParentPage{@StateuserInfo:UserInfo=newUserInfo()build(){Column({space:20}){Text(`姓名:${this.userInfo.name}`).fontSize(18)ChildEditor({user:this.userInfo})}}}@Componentstruct ChildEditor{@Linkuser:UserInfo// ⚠️ 使用 @Link 双向绑定build(){TextInput({text:this.user.name}).onChange((value:string)=>{this.user.name=value// ❌ 父组件 UI 不会刷新!})}}问题原因:@Link虽然支持双向绑定,但对于复杂对象的深层属性修改(如this.user.name = value),框架无法感知到对象内部属性的变化,因此不会触发父组件的 UI 刷新。
二、解决方案:@Observed + @ObjectLink
2.1 核心概念
| 装饰器 | 作用对象 | 功能说明 |
|---|---|---|
@Observed | 类(class) | 标记该类的实例可被深度观测,其属性变化可以被框架捕获 |
@ObjectLink | 组件属性 | 在子组件中建立对@Observed对象的引用代理,实现深层属性监听 |
2.2 协作原理
父组件 @State 持有对象 ↓ 传递给子组件 子组件 @ObjectLink 接收 ↓ 建立属性代理 修改 @ObjectLink 对象的属性 ↓ @Observed 拦截变化 触发依赖该属性的 UI 刷新关键理解:
@Observed给对象套上"拦截器",任何属性变更都会被捕获@ObjectLink不是数据拷贝,而是指向同一个被观测对象的代理引用- 两者配合使用,实现跨组件的深层属性响应式
三、@Observed 装饰器详解
3.1 基本用法
@Observed装饰在类定义上,使该类的实例对象属性可被观测:
@ObservedclassProduct{name:stringprice:numberquantity:numberconstructor(name:string,price:number,quantity:number){this.name=namethis.price=pricethis.quantity=quantity}}3.2 使用规则
| 规则 | 说明 |
|---|---|
| 仅装饰 class | 不能装饰 interface、type 或普通变量 |
| 支持嵌套观测 | 如果属性是另一个@Observed类的实例,深层属性也可被观测 |
| 配合 @State 或 @ObjectLink | 在组件中需用@State持有实例,或用@ObjectLink接收 |
| 支持数组场景 | @State arr: ObservedClass[]可观测数组增删和元素属性变化 |
3.3 嵌套 @Observed
当对象包含另一个对象作为属性时,内层对象也需要 @Observed 装饰:
@ObservedclassAddress{city:string='北京'district:string='朝阳区'}@ObservedclassUser{name:string='张三'address:Address=newAddress()// Address 也必须 @Observed}如果不给Address加@Observed,修改user.address.city将无法触发 UI 更新。
四、@ObjectLink 装饰器详解
4.1 基本用法
@ObjectLink装饰在子组件的属性上,用于接收父组件传递的@Observed类实例:
@Componentstruct ProductCard{@ObjectLinkproduct:Product// 接收 @Observed 对象build(){Row(){Text(this.product.name)Text(`数量:${this.product.quantity}`)Button('+').onClick(()=>{this.product.quantity++// ✅ UI 自动刷新})}}}4.2 使用规则
| 规则 | 说明 |
|---|---|
| 仅在子组件中使用 | 不能在@Entry组件中使用@ObjectLink |
| 必须接收 @Observed 实例 | 父组件传递的对象必须是@Observed类的实例 |
| 不支持初始化赋值 | @ObjectLink不能在声明时赋值,必须由父组件传入 |
传递时不加$ | 与@Link不同,传递@ObjectLink对象时不需要$前缀 |
4.3 @ObjectLink vs @Link 对比
| 对比项 | @Link | @ObjectLink |
|---|---|---|
| 数据类型 | 基本类型、对象均可 | 仅支持@Observed类实例 |
| 深层属性观测 | ❌ 不支持 | ✅ 支持 |
| 整体替换对象 | ✅ 支持 | ❌ 不支持 |
| 传递方式 | 需要$前缀 | 不需要$前缀 |
| 初始化 | 父组件传值 | 父组件传值 |
选择标准:
- 传递基本类型(string、number)→ 用
@Link - 传递复杂对象且需观测内部属性变化→ 用
@Observed+@ObjectLink - 需要整体替换对象→ 用
@Link
五、完整实战示例:购物车商品管理
5.1 效果说明
- 父组件显示商品列表和总价
- 子组件展示每个商品并提供加减数量按钮
- 修改商品数量后,父组件的总价自动同步更新
- 支持多层嵌套:父 → 子 → 孙组件均能感知变化
5.2 数据模型定义
// CartItemModel.ets@ObservedexportclassCartItem{id:numbername:stringprice:numberquantity:numberconstructor(id:number,name:string,price:number){this.id=idthis.name=namethis.price=pricethis.quantity=1}}5.3 父组件代码
// CartPage.etsimport{CartItem}from'../model/CartItemModel'@Entry@Componentstruct CartPage{@StatecartItems:CartItem[]=[newCartItem(1,'无线蓝牙耳机',299),newCartItem(2,'手机保护壳',39),newCartItem(3,'Type-C 数据线',19),newCartItem(4,'充电宝',129),]gettotalPrice():number{returnthis.cartItems.reduce((sum,item)=>sum+item.price*item.quantity,0)}gettotalCount():number{returnthis.cartItems.reduce((sum,item)=>sum+item.quantity,0)}build(){Column(){// 标题Text('购物车').fontSize(22).fontWeight(FontWeight.Bold).width('100%').padding({left:16,top:16,bottom:12})// 商品列表List({space:8}){ForEach(this.cartItems,(item:CartItem)=>{ListItem(){CartItemRow({cartItem:item})// 直接传递,不加 $}},(item:CartItem)=>item.id.toString())}.layoutWeight(1).scrollBar(BarState.Off).padding({left:16,right:16})// 底部汇总栏Row(){Column(){Text(`共${this.totalCount}件商品`).fontSize(13).fontColor('#888888')Text(`合计:¥${this.totalPrice}`).fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FF5722')}.layoutWeight(1).alignItems(HorizontalAlign.Start)Button('结算').fontSize(16).backgroundColor('#FF5722').fontColor(Color.White).width(120)}.height(70).padding({left:16,right:16}).backgroundColor(Color.White)}.width('100%').height('100%').backgroundColor('#F5F5F5')}}5.4 子组件代码
// CartItemRow.etsimport{CartItem}from'../model/CartItemModel'@Componentstruct CartItemRow{@ObjectLinkcartItem:CartItem// 深层观测对象属性变化build(){Row(){// 商品信息Column(){Text(this.cartItem.name).fontSize(15).fontWeight(FontWeight.Medium)Text(`¥${this.cartItem.price}`).fontSize(14).fontColor('#FF5722').margin({top:4})}.layoutWeight(1).alignItems(HorizontalAlign.Start)// 数量控制Row({space:12}){Text('−').fontSize(20).width(32).height(32).textAlign(TextAlign.Center).backgroundColor('#EEEEEE').borderRadius(16).onClick(()=>{if(this.cartItem.quantity>1){this.cartItem.quantity--// ✅ 父组件总价自动更新}})Text(`${this.cartItem.quantity}`).fontSize(16).width(30).textAlign(TextAlign.Center)Text('+').fontSize(20).width(32).height(32).textAlign(TextAlign.Center).backgroundColor('#EEEEEE').borderRadius(16).onClick(()=>{this.cartItem.quantity++// ✅ 父组件总价自动更新})}}.width('100%').padding(12).backgroundColor(Color.White).borderRadius(8)}}5.5 关键代码解析
(1)@Observed 标记数据模型
@ObservedexportclassCartItem{quantity:number=1}@Observed让CartItem类的实例属性(如quantity)变化可被框架拦截。
(2)@State 持有数组
@StatecartItems:CartItem[]=[...]@State装饰数组可以观测数组整体的增删操作(如push、splice)。配合@Observed修饰的CartItem,数组中每个元素的属性变化也可被观测。
(3)@ObjectLink 接收并深层观测
@ObjectLinkcartItem:CartItem子组件通过@ObjectLink接收CartItem实例。修改this.cartItem.quantity时,框架会通知所有依赖该属性的组件刷新,包括父组件中的总价计算。
(4)传递时不加 $
CartItemRow({cartItem:item})// ✅ 正确// CartItemRow({ cartItem: $item }) // ❌ 错误,@ObjectLink 不需要 $六、多层嵌套场景
6.1 三层传递示例
当数据需要从爷爷组件传到孙子组件时,每一层都使用@ObjectLink即可:
@ObservedclassOrderInfo{id:string='ORD001'status:string='待发货'}// 爷爷组件@Entry@Componentstruct OrderPage{@Stateorder:OrderInfo=newOrderInfo()build(){Column(){Text(`订单状态:${this.order.status}`)OrderDetail({order:this.order})// 传给儿子}}}// 儿子组件@Componentstruct OrderDetail{@ObjectLinkorder:OrderInfobuild(){Column(){Text(`订单号:${this.order.id}`)OrderAction({order:this.order})// 继续传给孙子}}}// 孙子组件@Componentstruct OrderAction{@ObjectLinkorder:OrderInfobuild(){Button('确认收货').onClick(()=>{this.order.status='已完成'// ✅ 爷爷组件的状态也自动更新})}}6.2 传递链路图
爷爷(@State 持有) ↓ 直接传递实例 儿子(@ObjectLink 接收) ↓ 继续传递 孙子(@ObjectLink 接收) ↓ 修改属性 所有依赖该属性的组件自动刷新七、常见陷阱与解决方案
7.1 陷阱一:忘记给内层类加 @Observed
// ❌ 错误写法@ObservedclassUser{name:string='张三'address:Address=newAddress()// Address 没加 @Observed}classAddress{// ❌ 缺少 @Observedcity:string='北京'}解决:所有需要被观测属性的嵌套类都必须加@Observed。
7.2 陷阱二:在 @Entry 组件使用 @ObjectLink
// ❌ 错误写法@Entry@Componentstruct MyPage{@ObjectLinkitem:Product// @Entry 不能使用 @ObjectLink}解决:@ObjectLink只能在非@Entry的子组件中使用。
7.3 陷阱三:@Link 和 @ObjectLink 混用
// ❌ 错误写法 - 用 @Link 传对象然后修改深层属性@Componentstruct ChildView{@Linkitem:Productbuild(){Button('改名称').onClick(()=>{this.item.name='新品'// ⚠️ 可能不触发 UI 刷新})}}解决:复杂对象深层属性修改,统一使用@Observed+@ObjectLink。
7.4 陷阱四:@ObjectLink 传递时加了 $
// ❌ 错误写法CartItemRow({cartItem:$item})// ✅ 正确写法CartItemRow({cartItem:item})八、最佳实践总结
| 场景 | 推荐方案 |
|---|---|
| 基本类型双向同步 | @Link |
| 对象深层属性监听 | @Observed+@ObjectLink |
| 数组元素属性变化 | @Observed类 +@State数组 +@ObjectLink子组件 |
| 多层嵌套传递 | 每层都用@ObjectLink |
| 整体替换对象 | @Link(不是@ObjectLink) |
核心记忆口诀:
简单类型用
@Link,复杂对象@Observed。
子组件接@ObjectLink,深层属性全都能刷新。
嵌套类也要@Observed,传递不加$记心间。
九、@Observed 与 @ObservedV2 的兼容性
在混合使用 V1 和 V2 状态管理时,装饰器版本选择至关重要:
9.1 核心规则
| 场景 搭配 | 结果 |
|---|---|
@State(V1) +@Observed类 | ✅ 兼容,@State可持有@Observed实例 |
@State(V1) +@ObservedV2类 | ❌ 编译错误10905348,@State不能持有@ObservedV2类 |
@Local(V2) +@ObservedV2类 | ✅ 兼容,V2 组件使用 V2 数据模型 |
@ComponentV2子组件 +@Observed对象 | ⚠️@Param不深度观测,需手动刷新(见下方) |
9.2 @ComponentV2 + @Param 的刷新策略
当 V2 子组件通过@Param接收@Observed对象时,子组件不会自动观测对象内部属性的变化。需要两个补充手段:
ForEach key 包含状态值:让 key 随属性变化而变化,触发列表项重建
ForEach(items,(item:Item)=>{...},(item:Item)=>item.id.toString()+'_'+(item.isChecked?'1':'0'))父组件重新赋值数组引用:
[...array]让@Param检测到新值this.currentItems=[...this.currentItems]
9.3 选择建议
- 纯 V1 项目:使用
@Observed+@ObjectLink,深层观测开箱即用 - 纯 V2 项目:使用
@ObservedV2+@Trace+@Local,细粒度响应式 - V1/V2 混用:数据模型用
@Observed(V1),子组件用@ComponentV2+@Param,配合上述刷新策略
十、参考链接
- @Observed 和 @ObjectLink 官方文档
- 状态管理概述
- 装饰器总结与对比