📖鸿蒙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装饰的对象 | 子组件 | 类似@State,但专门用于接收@Observed对象 |
工作原理:
@Observed装饰的类,其所有属性都会被框架"代理"(类似Vue的reactive)@ObjectLink在子组件中接收这个对象,并建立双向监听当被监听对象的任意属性变化时,子组件自动刷新UI
3.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.stock,UI自动更新
运行效果:
点击商品可以展开/收起详情
在SKU卡片上点击+/-按钮修改库存
库存数字实时更新,总库存统计也同步更新
库存不足5件时,数字显示为红色警告
五、@State vs @Observed/@ObjectLink对比表
特性 | @State | @Observed + @ObjectLink |
|---|---|---|
监听深度 | 仅第一层属性 | 可监听任意深度的嵌套属性 |
对象属性修改 | 需要整体替换对象才能触发UI更新 | 直接修改属性即可触发UI更新 |
使用方式 | 父组件直接使用 | @Observed装饰类,@ObjectLink在子组件中使用 |
适用场景 | 简单对象、基础类型 | 嵌套对象、复杂数据结构 |
代码复杂度 | 简单直接 | 需要定义@Observed类和@ObjectLink变量 |
性能 | 整体替换开销较大 | 按需更新,性能更优 |
子组件通信 | 需要通过回调函数 | 子组件可直接修改对象属性 |
选择建议:
数据结构简单(基础类型、单层对象) → 使用
@State数据结构嵌套(对象包含对象/数组) → 使用
@Observed+@ObjectLink需要跨组件共享状态 → 考虑
@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:在父组件中使用@ObjectLink
@Entry @Component struct ParentPage { // 错误:@ObjectLink只能在子组件中使用 @ObjectLink product: Product = new Product() }正确写法:父组件使用@State,子组件使用@ObjectLink
@Entry @Component struct ParentPage { @State product: Product = new Product() // 父组件用@State } @Component struct ChildComponent { @ObjectLink product: Product // 子组件用@ObjectLink }错误3:@Observed类缺少构造函数
@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
最佳实践:避免常见错误,合理设计数据模型
核心记忆点:
遇到嵌套对象 → 想到
@Observed+@ObjectLink@Observed装饰类定义,@ObjectLink装饰子组件变量子组件可以直接修改对象属性,UI自动更新
系列文章推荐
序号 | 文章标题 | 适合人群 |
|---|---|---|
01 | 鸿蒙NEXT开发从零到一 | 零基础入门 |
02 | ArkUI组件库完全指南 | 入门进阶 |
03 | 状态管理一文通 | 状态管理基础 |
04 | 数据持久化与网络请求全攻略 | 数据层开发 |
05 | 性能优化实战指南 | 性能优化 |
06 | HarmonyOS API24 Beta新特性全解析 | 跟进最新特性 |
07 | 鸿蒙生态装机量破千万开发者薪资报告 | 行业趋势 |
08 | 鸿蒙NEXT开发环境搭建全攻略 | 环境配置 |
09 | ArkTS语法速成 | 语法基础 |
10 | 鸿蒙面试题TOP30 | 面试准备 |
11 | ArkUI组件库完全指南 | UI进阶 |
12 | 鸿蒙布局终极指南 | 布局技巧 |
13 | ArkUI高级布局技巧 | 高级布局 |
14 | ArkUI电商首页实战 | 综合实战 |
15 | DevEco Studio必备工具清单 | 开发工具 |
17 | @Observed和@ObjectLink嵌套对象(本文) | 进阶状态管理 |
标签:@Observed@ObjectLink鸿蒙状态管理嵌套对象ArkUI鸿蒙NEXT数据响应式子组件通信
📝作者:鸿蒙开发博客系列 |更新时间:2025年 💡如有问题:欢迎在评论区留言交流