鸿蒙NEXT开发实战系列| 第17篇 | 进阶篇 适合人群了解基础状态管理的开发者 ⏰阅读时间约12分钟 | 开发环境DevEco Studio 5.0上一篇DevEco Studio必备工具清单 | 下一篇敬请期待目录一、引言为什么需要Observed和ObjectLink二、State处理嵌套对象的痛点三、Observed和ObjectLink原理剖析四、嵌套对象实战商品列表商品详情五、State vs Observed/ObjectLink对比表六、最佳实践与避坑指南七、总结与系列推荐一、引言为什么需要Observed和ObjectLink在鸿蒙NEXT的状态管理中State是最基础也最常用的装饰器。但当你遇到嵌套对象的场景时State就会力不从心。什么是嵌套对象举个例子// 商品对象内部包含SKU数组 interface Product { id: number name: string skus: Sku[] // 嵌套的SKU数组 } interface Sku { id: number color: string stock: number }这种对象包含对象/数组的结构在实际开发中非常常见比如用户信息中的地址列表订单中的商品明细评论中的回复列表购物车中的商品项核心问题当你用State装饰Product对象然后修改skus[0].stock时UI不会更新这就是Observed和ObjectLink要解决的问题。二、State处理嵌套对象的痛点2.1 问题复现State无法监听嵌套属性变化先来看一个反面教材体验一下State在嵌套对象场景下的无力感// 数据模型定义 class Sku { id: number color: string stock: number constructor(id: number, color: string, stock: number) { this.id id this.color color this.stock stock } } class Product { id: number name: string skus: Sku[] constructor(id: number, name: string, skus: Sku[]) { this.id id this.name name this.skus skus } } Entry Component struct BrokenDemo { // 使用State装饰嵌套对象 State product: Product new Product(1, 鸿蒙手机壳, [ new Sku(1, 星空黑, 10), new Sku(2, 极光蓝, 5) ]) build() { Column() { Text(this.product.name) .fontSize(20) .fontWeight(FontWeight.Bold) .margin({ bottom: 20 }) ForEach(this.product.skus, (sku: Sku) { Row() { Text(${sku.color}: 库存${sku.stock}) .fontSize(16) .width(60%) Button(减库存) .onClick(() { // 问题代码直接修改嵌套对象的属性 sku.stock-- console.info(库存变为: ${sku.stock}) // 但是UI不会更新 }) } .width(100%) .justifyContent(Content.SpaceBetween) .padding(10) }) } .width(100%) .padding(20) } }问题分析点击减库存按钮后虽然sku.stock的值确实变了控制台能看到输出但界面显示的库存数字不会更新。根本原因State只能监听第一层属性的变化。对于嵌套对象它只能检测product引用是否改变而sku.stock xxx只是修改了内部属性product的引用并没有变化所以框架认为状态没有变化不会触发UI刷新。2.2 笨办法手动整体替换你可能想到一个笨办法——每次修改嵌套属性时都整体替换整个对象Button(减库存) .onClick(() { // 笨办法整体替换整个product对象 const newSkus this.product.skus.map(s { if (s.id sku.id) { return new Sku(s.id, s.color, s.stock - 1) } return s }) this.product new Product(this.product.id, this.product.name, newSkus) })这确实能解决问题但代码会变得非常繁琐。如果嵌套层级更深比如订单 商品 SKU 规格属性代码复杂度会指数级增长。这时候Observed和ObjectLink就该登场了。三、Observed和ObjectLink原理剖析3.1 核心概念装饰器作用使用位置说明Observed装饰类使其属性变化可被监听类定义让嵌套对象的属性变化能够被框架感知ObjectLink装饰变量监听被Observed装饰的对象子组件类似State但专门用于接收Observed对象工作原理Observed装饰的类其所有属性都会被框架代理类似Vue的reactiveObjectLink在子组件中接收这个对象并建立双向监听当被监听对象的任意属性变化时子组件自动刷新UI3.2 基础用法示例// 第一步使用Observed装饰类 Observed class Person { name: string age: number constructor(name: string, age: number) { this.name name this.age age } } // 第二步子组件使用ObjectLink接收对象 Component struct PersonCard { ObjectLink person: Person // 使用ObjectLink接收 build() { Row() { Text(this.person.name) .fontSize(18) .width(40%) Text(年龄: ${this.person.age}) .fontSize(16) .width(30%) Button(加一岁) .onClick(() { // 直接修改属性UI会自动更新 this.person.age }) .width(30%) } .width(100%) .padding(10) } } // 第三步父组件传递Observed对象 Entry Component struct PersonDemo { State person: Person new Person(张三, 25) build() { Column() { PersonCard({ person: this.person }) .margin({ bottom: 20 }) Button(父组件重置年龄为20) .onClick(() { this.person.age 20 }) } .width(100%) .padding(20) } }关键点Observed装饰类定义ObjectLink装饰子组件中的变量父组件中仍然使用State子组件使用ObjectLink子组件可以直接修改对象属性UI会自动更新四、嵌套对象实战商品列表商品详情现在让我们用一个完整的实战案例来展示Observed和ObjectLink的威力。4.1 场景描述实现一个商品库存管理系统商品列表页面显示多个商品每个商品可展开查看详情商品详情组件显示商品的SKU列表支持修改每个SKU的库存任何层级的修改都能实时反映到UI上4.2 完整代码实现// // 数据模型定义 // /** SKU规格 - 使用Observed装饰使其属性变化可被监听 */ Observed class SkuItem { id: number color: string size: string stock: number price: number constructor(id: number, color: string, size: string, stock: number, price: number) { this.id id this.color color this.size size this.stock stock this.price price } } /** 商品模型 */ class Product { id: number name: string image: string skus: SkuItem[] constructor(id: number, name: string, image: string, skus: SkuItem[]) { this.id id this.name name this.image image this.skus skus } } // // 子组件SKU卡片 - 使用ObjectLink接收被Observed装饰的对象 // Component struct SkuCard { ObjectLink sku: SkuItem // 使用ObjectLink接收Observed对象 build() { Row() { // 左侧规格信息 Column() { Text(${this.sku.color} / ${this.sku.size}) .fontSize(14) .fontWeight(FontWeight.Medium) Text(¥${this.sku.price}) .fontSize(12) .fontColor(#FF4D4F) .margin({ top: 4 }) } .alignItems(HorizontalAlign.Start) .layoutWeight(1) // 右侧库存操作 Row() { Button(-) .width(30) .height(30) .fontSize(16) .backgroundColor(#F5F5F5) .fontColor(#333) .onClick(() { if (this.sku.stock 0) { this.sku.stock-- // 直接修改UI自动更新 } }) Text(${this.sku.stock}) .fontSize(16) .fontWeight(FontWeight.Bold) .width(50) .textAlign(TextAlign.Center) .fontColor(this.sku.stock 5 ? #FF4D4F : #333) Button() .width(30) .height(30) .fontSize(16) .backgroundColor(#1890FF) .fontColor(Color.White) .onClick(() { this.sku.stock // 直接修改UI自动更新 }) } } .width(100%) .padding(12) .backgroundColor(#FAFAFA) .borderRadius(8) .margin({ bottom: 8 }) } } // // 子组件商品详情 - 展示商品信息和SKU列表 // Component struct ProductDetail { Prop product!: Product // 接收整个商品对象 build() { Column() { // 商品标题 Text(this.product.name) .fontSize(18) .fontWeight(FontWeight.Bold) .margin({ bottom: 12 }) // SKU统计信息 Row() { Text(共 ${this.product.skus.length} 个规格) .fontSize(12) .fontColor(#999) Text(总库存: ${this.getTotalStock()}) .fontSize(12) .fontColor(#1890FF) .margin({ left: 16 }) } .margin({ bottom: 12 }) // SKU列表 - 将每个SkuItem传递给SkuCard ForEach(this.product.skus, (sku: SkuItem) { SkuCard({ sku: sku }) // 传递Observed对象 }) } .width(100%) .padding(16) .backgroundColor(Color.White) .borderRadius(12) .shadow({ radius: 4, color: #1A000000, offsetY: 2 }) } /** 计算总库存 */ getTotalStock(): number { return this.product.skus.reduce((sum, sku) sum sku.stock, 0) } } // // 主页面商品列表 // Entry Component struct ProductListPage { // 商品列表数据 State products: Product[] [ new Product(1, 鸿蒙限定手机壳, , [ new SkuItem(1, 星空黑, 标准版, 10, 99), new SkuItem(2, 极光蓝, 标准版, 5, 99), new SkuItem(3, 樱落粉, Pro版, 8, 129) ]), new Product(2, 鸿蒙开发手册, , [ new SkuItem(4, 纸质版, 入门篇, 20, 59), new SkuItem(5, 电子版, 进阶篇, 999, 39) ]), new Product(3, 鸿蒙主题T恤, , [ new SkuItem(6, 深空灰, S码, 3, 199), new SkuItem(7, 深空灰, M码, 7, 199), new SkuItem(8, 深空灰, L码, 2, 199) ]) ] State expandedId: number -1 // 当前展开的商品ID build() { Column() { // 页面标题 Text(库存管理系统) .fontSize(24) .fontWeight(FontWeight.Bold) .margin({ bottom: 20 }) // 统计信息 this.StatBar() // 商品列表 List({ space: 16 }) { ForEach(this.products, (product: Product) { ListItem() { Column() { // 商品头部点击展开/收起 Row() { Text(product.image) .fontSize(24) .margin({ right: 8 }) Text(product.name) .fontSize(16) .fontWeight(FontWeight.Medium) .layoutWeight(1) Text(this.expandedId product.id ? 收起 : 展开) .fontSize(12) .fontColor(#1890FF) } .width(100%) .onClick(() { this.expandedId this.expandedId product.id ? -1 : product.id }) // 展开后显示商品详情 if (this.expandedId product.id) { ProductDetail({ product: product }) .margin({ top: 12 }) .transition(TransitionEffect.OPACITY.animation({ duration: 200 })) } } .width(100%) .padding(16) .backgroundColor(Color.White) .borderRadius(12) .shadow({ radius: 4, color: #1A000000, offsetY: 2 }) } }) } .layoutWeight(1) .width(100%) } .width(100%) .height(100%) .padding(16) .backgroundColor(#F5F5F5) } /** 顶部统计栏 */ Builder StatBar() { Row() { this.StatItem(商品总数, ${this.products.length}件) this.StatItem(规格总数, ${this.getTotalSku()}个) this.StatItem(总库存, ${this.getTotalStock()}) } .width(100%) .justifyContent(Content.SpaceAround) .padding(16) .backgroundColor(Color.White) .borderRadius(12) .margin({ bottom: 16 }) } Builder StatItem(label: string, value: string) { Column() { Text(value) .fontSize(20) .fontWeight(FontWeight.Bold) .fontColor(#1890FF) Text(label) .fontSize(12) .fontColor(#999) .margin({ top: 4 }) } } /** 计算总SKU数 */ getTotalSku(): number { return this.products.reduce((sum, p) sum p.skus.length, 0) } /** 计算总库存 */ getTotalStock(): number { return this.products.reduce((sum, p) { return sum p.skus.reduce((skuSum, sku) skuSum sku.stock, 0) }, 0) } }4.3 代码解析数据流向父组件 (State products) ↓ 传递整个Product对象 子组件 ProductDetail (Prop product) ↓ 传递单个SkuItem对象 孙组件 SkuCard (ObjectLink sku) ↓ 直接修改属性 UI自动更新关键代码说明Observed装饰类第8行让SkuItem的属性变化可被监听ObjectLink接收对象第72行子组件使用ObjectLink接收被Observed装饰的对象直接修改属性第93行在子组件中直接修改sku.stockUI自动更新运行效果点击商品可以展开/收起详情在SKU卡片上点击/-按钮修改库存库存数字实时更新总库存统计也同步更新库存不足5件时数字显示为红色警告五、State vs Observed/ObjectLink对比表特性StateObserved ObjectLink监听深度仅第一层属性可监听任意深度的嵌套属性对象属性修改需要整体替换对象才能触发UI更新直接修改属性即可触发UI更新使用方式父组件直接使用Observed装饰类ObjectLink在子组件中使用适用场景简单对象、基础类型嵌套对象、复杂数据结构代码复杂度简单直接需要定义Observed类和ObjectLink变量性能整体替换开销较大按需更新性能更优子组件通信需要通过回调函数子组件可直接修改对象属性选择建议数据结构简单基础类型、单层对象 → 使用State数据结构嵌套对象包含对象/数组 → 使用ObservedObjectLink需要跨组件共享状态 → 考虑Provide/Consume六、最佳实践与避坑指南6.1 常见错误错误1忘记添加Observed装饰器// 错误类没有使用Observed装饰 class SkuItem { stock: number 10 } // 子组件使用ObjectLink会编译报错 Component struct SkuCard { ObjectLink sku: SkuItem // 编译错误 }正确写法Observed // 必须添加 class SkuItem { stock: number 10 }错误2在父组件中使用ObjectLinkEntry Component struct ParentPage { // 错误ObjectLink只能在子组件中使用 ObjectLink product: Product new Product() }正确写法父组件使用State子组件使用ObjectLinkEntry Component struct ParentPage { State product: Product new Product() // 父组件用State } Component struct ChildComponent { ObjectLink product: Product // 子组件用ObjectLink }错误3Observed类缺少构造函数Observed class SkuItem { stock: number // 未初始化使用时会报undefined }正确写法Observed class SkuItem { stock: number 0 // 提供默认值 constructor(stock: number) { this.stock stock } }6.2 最佳实践1. 合理设计数据模型// 推荐明确区分Observed类和普通接口 Observed class ObservableProduct { id: number name: string items: ObservableSku[] constructor(id: number, name: string, items: ObservableSku[]) { this.id id this.name name this.items items } } Observed class ObservableSku { id: number stock: number constructor(id: number, stock: number) { this.id id this.stock stock } }2. 子组件职责单一// 推荐子组件只负责展示和交互不处理复杂业务逻辑 Component struct StockCounter { ObjectLink sku: ObservableSku build() { Row() { Button(-) .onClick(() { if (this.sku.stock 0) { this.sku.stock-- } }) Text(${this.sku.stock}) .width(50) .textAlign(TextAlign.Center) Button() .onClick(() { this.sku.stock }) } } }3. 使用计算属性获取派生数据Component struct ProductSummary { Prop product!: ObservableProduct build() { Column() { Text(商品名称: ${this.product.name}) Text(SKU数量: ${this.product.items.length}) Text(总库存: ${this.getTotalStock()}) } } // 使用getter获取派生数据 getTotalStock(): number { return this.product.items.reduce((sum, item) sum item.stock, 0) } }6.3 性能优化建议避免不必要的Observed如果对象属性不会被修改不需要添加Observed合理使用ForEach的key为ForEach提供稳定的key避免不必要的重渲染控制监听粒度只在需要监听的类上添加Observed不要滥用七、总结与系列推荐本文总结通过本文你应该掌握了以下内容State的局限性只能监听第一层属性对嵌套对象无能为力Observed和ObjectLink的原理Observed装饰类使其属性可监听ObjectLink在子组件中接收并监听实战应用在商品库存管理场景中使用Observed和ObjectLink最佳实践避免常见错误合理设计数据模型核心记忆点遇到嵌套对象 → 想到ObservedObjectLinkObserved装饰类定义ObjectLink装饰子组件变量子组件可以直接修改对象属性UI自动更新系列文章推荐序号文章标题适合人群01鸿蒙NEXT开发从零到一零基础入门02ArkUI组件库完全指南入门进阶03状态管理一文通状态管理基础04数据持久化与网络请求全攻略数据层开发05性能优化实战指南性能优化06HarmonyOS API24 Beta新特性全解析跟进最新特性07鸿蒙生态装机量破千万开发者薪资报告行业趋势08鸿蒙NEXT开发环境搭建全攻略环境配置09ArkTS语法速成语法基础10鸿蒙面试题TOP30面试准备11ArkUI组件库完全指南UI进阶12鸿蒙布局终极指南布局技巧13ArkUI高级布局技巧高级布局14ArkUI电商首页实战综合实战15DevEco Studio必备工具清单开发工具17Observed和ObjectLink嵌套对象本文进阶状态管理标签ObservedObjectLink鸿蒙状态管理嵌套对象ArkUI鸿蒙NEXT数据响应式子组件通信作者鸿蒙开发博客系列 |更新时间2025年 如有问题欢迎在评论区留言交流