欢迎光临
我们一直在努力

@Observed和@ObjectLink到底怎么用?鸿蒙嵌套对象状态管理的终极解决方案

📖 鸿蒙NEXT开发实战系列 | 第17篇 | 进阶篇 🎯 适合人群:了解基础状态管理的开发者 ⏰ 阅读时间:约12分钟 | 💻 开发环境:DevEco Studio 5.0+

上一篇:DevEco Studio必备工具清单 | 下一篇:敬请期待


目录

  • 一、引言:为什么需要@Observed和@ObjectLink

  • 二、@State处理嵌套对象的痛点

  • 三、@Observed和@ObjectLink原理剖析

  • 四、嵌套对象实战:商品列表+商品详情

  • 五、@State vs @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的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年 💡 如有问题:欢迎在评论区留言交流

    赞(0)
    未经允许不得转载:171主机测评 » @Observed和@ObjectLink到底怎么用?鸿蒙嵌套对象状态管理的终极解决方案
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址