《级联年度选择》三、Observed与ObjectLink使用指南

📅 2026/7/17 21:21:59 👁️ 阅读次数 📝 编程学习
《级联年度选择》三、Observed与ObjectLink使用指南

HarmonyOS ArkTS @Observed 与 @ObjectLink 装饰器:嵌套类对象属性变化使用指南

本文深入讲解 ArkUI 状态管理中@Observed@ObjectLink装饰器的核心原理、使用方法及常见陷阱。通过实际案例帮助你理解嵌套对象属性变化的监听机制,避免状态不刷新的"坑"。


效果

一、问题背景:@State 的观测局限

1.1 @State 能观测什么

@State装饰器可以监听基本数据类型stringnumberboolean)的值变化,以及对象/数组的第一层属性的赋值操作。

@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}

@ObservedCartItem类的实例属性(如quantity)变化可被框架拦截。

(2)@State 持有数组

@StatecartItems:CartItem[]=[...]

@State装饰数组可以观测数组整体的增删操作(如pushsplice)。配合@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对象时,子组件不会自动观测对象内部属性的变化。需要两个补充手段:

  1. ForEach key 包含状态值:让 key 随属性变化而变化,触发列表项重建

    ForEach(items,(item:Item)=>{...},(item:Item)=>item.id.toString()+'_'+(item.isChecked?'1':'0'))
  2. 父组件重新赋值数组引用[...array]@Param检测到新值

    this.currentItems=[...this.currentItems]

9.3 选择建议

  • 纯 V1 项目:使用@Observed+@ObjectLink,深层观测开箱即用
  • 纯 V2 项目:使用@ObservedV2+@Trace+@Local,细粒度响应式
  • V1/V2 混用:数据模型用@Observed(V1),子组件用@ComponentV2+@Param,配合上述刷新策略

十、参考链接

  • @Observed 和 @ObjectLink 官方文档
  • 状态管理概述
  • 装饰器总结与对比