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

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


目录


在鸿蒙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对象

工作原理

  1. @Observed装饰的类,其所有属性都会被框架"代理"(类似Vue的reactive)

  2. @ObjectLink在子组件中接收这个对象,并建立双向监听

  3. 当被监听对象的任意属性变化时,子组件自动刷新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自动更新

关键代码说明

  1. @Observed装饰类(第8行):让SkuItem的属性变化可被监听

  2. @ObjectLink接收对象(第72行):子组件使用@ObjectLink接收被@Observed装饰的对象

  3. 直接修改属性(第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 性能优化建议

  1. 避免不必要的@Observed:如果对象属性不会被修改,不需要添加@Observed

  2. 合理使用ForEach的key:为ForEach提供稳定的key,避免不必要的重渲染

  3. 控制监听粒度:只在需要监听的类上添加@Observed,不要滥用


七、总结与系列推荐

本文总结

通过本文,你应该掌握了以下内容:

  1. @State的局限性:只能监听第一层属性,对嵌套对象无能为力

  2. @Observed和@ObjectLink的原理:@Observed装饰类使其属性可监听,@ObjectLink在子组件中接收并监听

  3. 实战应用:在商品库存管理场景中使用@Observed和@ObjectLink

  4. 最佳实践:避免常见错误,合理设计数据模型

核心记忆点

  • 遇到嵌套对象 → 想到@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年 💡 如有问题:欢迎在评论区留言交流

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐