ArkTS 中 @Observed 和 @ObjectLink 装饰器总结

📌 快速概览

@Observed@ObjectLink 是 ArkTS 中用于嵌套对象深度观察的装饰器组合,解决了 @State@Prop@Link 只能观察第一层属性变化的问题。


🎯 一、核心概念

1.1 问题背景

@State 的局限性:只能观察第一层属性变化

class Person {
  name: string
  age: number

  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }
}

@Entry
@Component
struct TestPage {
  @State person: Person = new Person('张三', 25)

  build() {
    Column() {
      Text(`姓名: ${this.person.name}`)
      Text(`年龄: ${this.person.age}`)

      Button('修改姓名')
        .onClick(() => {
          // ❌ 不会触发 UI 更新(只观察第一层)
          this.person.name = '李四'
        })

      Button('整体替换')
        .onClick(() => {
          // ✅ 会触发 UI 更新(第一层变化)
          this.person = new Person('李四', 30)
        })
    }
  }
}

问题

  • @State 只观察第一层属性this.person
  • 修改嵌套属性this.person.name)不会触发 UI 更新
  • 必须整体替换对象才能触发更新,不够灵活

1.2 @Observed 装饰器

定义:类装饰器,用于修饰,使类的实例变为可观察对象

作用

  • 将普通类转换为响应式对象
  • 使对象的所有属性都可被观察
  • 支持深度观察(嵌套对象)

语法

@Observed
class Person {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

1.3 @ObjectLink 装饰器

定义:变量装饰器,用于修饰子组件中的变量,接收父组件传递的 @Observed 对象。

作用

  • 接收 @Observed 对象
  • 深度观察对象的所有属性变化
  • 实现父子组件双向数据同步

语法

@Component
struct ChildComponent {
  @ObjectLink person: Person  // 接收 @Observed 对象

  build() {
    Column() {
      Text(this.person.name)
    }
  }
}

1.4 工作原理

1. 父组件用 @State/@Prop/@Link 持有 @Observed 对象
   ↓
2. 将对象传递给子组件的 @ObjectLink 变量
   ↓
3. 子组件通过 @ObjectLink 深度观察对象的所有属性
   ↓
4. 任何属性变化都会触发 UI 更新

核心要点

  • @Observed:修饰,让类的实例可被观察
  • @ObjectLink:修饰变量,接收可观察对象并深度观察
  • 必须配合使用@Observed 类 + @ObjectLink 变量

📖 二、基本用法

2.1 简单示例

// 步骤 1:定义 @Observed 类
@Observed
class Person {
  name: string
  age: number

  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }
}

// 步骤 2:父组件用 @State 持有对象
@Entry
@Component
struct ParentPage {
  @State person: Person = new Person('张三', 25)

  build() {
    Column() {
      Text('父组件')
      Text(`姓名: ${this.person.name}`)
      Text(`年龄: ${this.person.age}`)

      // 步骤 3:传递给子组件
      ChildComponent({ person: this.person })

      Button('父组件修改姓名')
        .onClick(() => {
          // ✅ 触发 UI 更新
          this.person.name = '李四'
        })
    }
  }
}

// 步骤 4:子组件用 @ObjectLink 接收
@Component
struct ChildComponent {
  @ObjectLink person: Person

  build() {
    Column() {
      Text('子组件')
      Text(`姓名: ${this.person.name}`)
      Text(`年龄: ${this.person.age}`)

      Button('子组件修改年龄')
        .onClick(() => {
          // ✅ 触发父子组件 UI 同步更新
          this.person.age++
        })
    }
  }
}

效果

  • ✅ 父组件修改 person.name,父子组件都会更新
  • ✅ 子组件修改 person.age,父子组件都会更新
  • ✅ 双向数据同步

2.2 嵌套对象示例

// 嵌套类也需要 @Observed
@Observed
class Address {
  city: string
  street: string

  constructor(city: string, street: string) {
    this.city = city
    this.street = street
  }
}

@Observed
class Person {
  name: string
  address: Address  // 嵌套对象

  constructor(name: string, address: Address) {
    this.name = name
    this.address = address
  }
}

@Entry
@Component
struct ParentPage {
  @State person: Person = new Person('张三', new Address('北京', '朝阳路'))

  build() {
    Column() {
      Text(`姓名: ${this.person.name}`)
      Text(`城市: ${this.person.address.city}`)
      Text(`街道: ${this.person.address.street}`)

      // 传递给子组件
      ChildComponent({ person: this.person })

      Button('修改城市')
        .onClick(() => {
          // ✅ 深度观察,触发 UI 更新
          this.person.address.city = '上海'
        })
    }
  }
}

@Component
struct ChildComponent {
  @ObjectLink person: Person

  build() {
    Column() {
      Button('修改街道')
        .onClick(() => {
          // ✅ 深度观察,触发 UI 更新
          this.person.address.street = '浦东路'
        })
    }
  }
}

要点

  • 嵌套对象的类(Address)也需要加 @Observed
  • 支持多层嵌套的深度观察

2.3 数组中的对象

@Observed
class Student {
  name: string
  score: number

  constructor(name: string, score: number) {
    this.name = name
    this.score = score
  }
}

@Entry
@Component
struct StudentList {
  @State students: Student[] = [
    new Student('张三', 85),
    new Student('李四', 90),
    new Student('王五', 78)
  ]

  build() {
    Column() {
      List() {
        ForEach(this.students, (student: Student, index: number) => {
          ListItem() {
            // 传递数组中的对象给子组件
            StudentCard({ student: student, index: index })
          }
        }, (student: Student) => student.name)
      }

      Button('添加学生')
        .onClick(() => {
          // ✅ 数组变化,触发 UI 更新
          this.students.push(new Student('赵六', 88))
        })
    }
  }
}

@Component
struct StudentCard {
  @ObjectLink student: Student  // 接收数组中的对象
  private index: number

  build() {
    Row() {
      Text(`${this.index + 1}. ${this.student.name}`)
      Text(`分数: ${this.student.score}`)

      Button('+10')
        .onClick(() => {
          // ✅ 修改对象属性,触发 UI 更新
          this.student.score += 10
        })
    }
  }
}

要点

  • 数组中的对象也需要用 @Observed 修饰类
  • @ObjectLink 接收数组中的单个对象
  • 支持修改数组中对象的属性

🎯 三、使用场景

3.1 场景 1:表单组件

@Observed
class FormData {
  username: string = ''
  password: string = ''
  email: string = ''
  phone: string = ''
}

@Entry
@Component
struct RegisterPage {
  @State formData: FormData = new FormData()

  build() {
    Column() {
      Text('注册表单')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)

      // 用户名输入
      InputField({
        label: '用户名',
        value: this.formData.username,
        onChange: (value: string) => {
          this.formData.username = value
        }
      })

      // 密码输入
      InputField({
        label: '密码',
        value: this.formData.password,
        onChange: (value: string) => {
          this.formData.password = value
        }
      })

      // 预览组件
      FormPreview({ formData: this.formData })

      Button('提交')
        .onClick(() => {
          console.log(JSON.stringify(this.formData))
        })
    }
  }
}

@Component
struct InputField {
  private label: string
  private value: string
  private onChange: (value: string) => void

  build() {
    Column() {
      Text(this.label)
      TextInput({ text: this.value })
        .onChange((value: string) => {
          this.onChange(value)
        })
    }
  }
}

@Component
struct FormPreview {
  @ObjectLink formData: FormData

  build() {
    Column() {
      Text('表单预览')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)

      Text(`用户名: ${this.formData.username}`)
      Text(`密码: ${'*'.repeat(this.formData.password.length)}`)
    }
    .backgroundColor('#F5F5F5')
    .padding(10)
    .borderRadius(8)
  }
}

3.2 场景 2:购物车

@Observed
class Product {
  id: number
  name: string
  price: number
  quantity: number

  constructor(id: number, name: string, price: number) {
    this.id = id
    this.name = name
    this.price = price
    this.quantity = 1
  }

  get totalPrice(): number {
    return this.price * this.quantity
  }
}

@Entry
@Component
struct ShoppingCart {
  @State products: Product[] = [
    new Product(1, 'iPhone 15', 7999),
    new Product(2, 'iPad Pro', 6799),
    new Product(3, 'MacBook', 12999)
  ]

  get totalAmount(): number {
    return this.products.reduce((sum, p) => sum + p.totalPrice, 0)
  }

  build() {
    Column() {
      Text('购物车')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)

      List() {
        ForEach(this.products, (product: Product) => {
          ListItem() {
            ProductItem({ product: product })
          }
        }, (product: Product) => product.id.toString())
      }

      // 总价
      Row() {
        Text('总计:')
        Text(`¥${this.totalAmount}`)
          .fontSize(24)
          .fontColor('#FF0000')
          .fontWeight(FontWeight.Bold)
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)
      .padding(15)
      .backgroundColor('#FFF9E6')
    }
  }
}

@Component
struct ProductItem {
  @ObjectLink product: Product

  build() {
    Row() {
      Column() {
        Text(this.product.name)
          .fontSize(16)
          .fontWeight(FontWeight.Bold)

        Text(`¥${this.product.price}`)
          .fontSize(14)
          .fontColor('#FF4D4F')
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)

      // 数量控制
      Row() {
        Button('-')
          .onClick(() => {
            if (this.product.quantity > 1) {
              this.product.quantity--  // ✅ 触发 UI 更新
            }
          })

        Text(`${this.product.quantity}`)
          .width(40)
          .textAlign(TextAlign.Center)

        Button('+')
          .onClick(() => {
            this.product.quantity++  // ✅ 触发 UI 更新
          })
      }

      Text(`¥${this.product.totalPrice}`)
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
    }
    .width('100%')
    .padding(15)
    .backgroundColor(Color.White)
    .borderRadius(8)
  }
}

3.3 场景 3:用户资料编辑

@Observed
class UserProfile {
  avatar: string
  nickname: string
  bio: string
  age: number

  constructor() {
    this.avatar = ''
    this.nickname = '未设置'
    this.bio = '这个人很懒,什么都没写'
    this.age = 18
  }
}

@Entry
@Component
struct ProfilePage {
  @State profile: UserProfile = new UserProfile()

  build() {
    Column() {
      // 预览区域
      ProfilePreview({ profile: this.profile })

      // 编辑区域
      ProfileEditor({ profile: this.profile })

      Button('保存')
        .margin({ top: 20 })
        .onClick(() => {
          console.log('保存:', JSON.stringify(this.profile))
        })
    }
  }
}

@Component
struct ProfilePreview {
  @ObjectLink profile: UserProfile

  build() {
    Column() {
      Text('预览')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)

      Image(this.profile.avatar || 'default_avatar.png')
        .width(80)
        .height(80)
        .borderRadius(40)

      Text(this.profile.nickname)
        .fontSize(20)
        .fontWeight(FontWeight.Bold)

      Text(this.profile.bio)
        .fontSize(14)
        .fontColor(Color.Gray)

      Text(`年龄: ${this.profile.age}`)
    }
    .backgroundColor('#F5F5F5')
    .padding(20)
    .borderRadius(12)
  }
}

@Component
struct ProfileEditor {
  @ObjectLink profile: UserProfile

  build() {
    Column() {
      Text('编辑')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)

      TextInput({ placeholder: '昵称', text: this.profile.nickname })
        .onChange((value: string) => {
          this.profile.nickname = value  // ✅ 实时同步到预览
        })

      TextInput({ placeholder: '个人简介', text: this.profile.bio })
        .onChange((value: string) => {
          this.profile.bio = value  // ✅ 实时同步到预览
        })

      Row() {
        Text('年龄:')
        Button('-')
          .onClick(() => {
            if (this.profile.age > 1) {
              this.profile.age--  // ✅ 实时同步到预览
            }
          })
        Text(`${this.profile.age}`)
        Button('+')
          .onClick(() => {
            this.profile.age++  // ✅ 实时同步到预览
          })
      }
    }
  }
}

⚠️ 四、注意事项

4.1 必须配合使用

// ❌ 错误:类没有 @Observed
class Person {
  name: string
}

@Component
struct MyComponent {
  @ObjectLink person: Person  // ❌ 报错:Person 必须是 @Observed 类

  build() {
    Text(this.person.name)
  }
}

正确做法

// ✅ 正确:类必须加 @Observed
@Observed
class Person {
  name: string
}

@Component
struct MyComponent {
  @ObjectLink person: Person  // ✅ 正确

  build() {
    Text(this.person.name)
  }
}

4.2 不能初始化

@Component
struct MyComponent {
  // ❌ 错误:@ObjectLink 不能初始化
  @ObjectLink person: Person = new Person('张三', 25)

  build() {
    Text(this.person.name)
  }
}

正确做法

@Component
struct MyComponent {
  // ✅ 正确:由父组件传递,不能初始化
  @ObjectLink person: Person

  build() {
    Text(this.person.name)
  }
}

// 父组件传递
@Entry
@Component
struct ParentPage {
  @State person: Person = new Person('张三', 25)

  build() {
    MyComponent({ person: this.person })  // 传递对象
  }
}

4.3 不能传递 null 或 undefined

@Entry
@Component
struct ParentPage {
  @State person: Person | null = null

  build() {
    if (this.person) {
      // ✅ 正确:确保不是 null
      MyComponent({ person: this.person })
    } else {
      Text('加载中...')
    }
  }
}

4.4 嵌套对象都需要 @Observed

// ❌ 错误:嵌套类没有 @Observed
class Address {
  city: string;
}

@Observed
class Person {
  address: Address; // ❌ Address 也需要 @Observed
}

正确做法

// ✅ 正确:嵌套类也加 @Observed
@Observed
class Address {
  city: string;
}

@Observed
class Person {
  address: Address; // ✅ 正确
}

4.5 数组变化的处理

@Entry
@Component
struct TodoList {
  @State todos: Todo[] = []

  build() {
    Column() {
      Button('添加')
        .onClick(() => {
          // ✅ 正确:push 触发数组变化
          this.todos.push(new Todo('新任务'))
        })

      Button('删除第一个')
        .onClick(() => {
          // ✅ 正确:splice 触发数组变化
          this.todos.splice(0, 1)
        })

      Button('修改第一个任务名')
        .onClick(() => {
          // ✅ 正确:修改对象属性触发更新
          this.todos[0].title = '已修改'
        })
    }
  }
}

📊 五、与其他装饰器对比

5.1 @State vs @ObjectLink

特性 @State @ObjectLink
使用位置 父组件 子组件
观察深度 第一层(浅观察) 所有层(深度观察)
是否初始化 必须初始化 不能初始化,由父组件传递
数据流向 组件内部状态 父子双向同步
对象类型要求 任意类型 必须是 @Observed 类

5.2 @Prop vs @ObjectLink

特性 @Prop @ObjectLink
数据流向 单向(父 → 子) 双向(父 ↔ 子)
观察深度 第一层(浅观察) 所有层(深度观察)
是否可修改 子组件只读 子组件可读写
对象类型要求 任意类型 必须是 @Observed 类
传递方式 值传递(复制) 引用传递(共享)

5.3 @Link vs @ObjectLink

特性 @Link @ObjectLink
数据流向 双向(父 ↔ 子) 双向(父 ↔ 子)
观察深度 第一层(浅观察) 所有层(深度观察)
传递方式 使用 $ 语法 直接传递对象
对象类型要求 任意类型 必须是 @Observed 类
使用复杂度 需要 $ 语法 直接传递,更简洁

5.4 完整对比示例

@Observed
class Person {
  name: string
  age: number

  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }
}

@Entry
@Component
struct ComparisonPage {
  @State person: Person = new Person('张三', 25)
  @State count: number = 0

  build() {
    Column() {
      // 1. @State:父组件持有对象
      Text(`父组件 @State: ${this.person.name}, ${this.person.age}`)

      Button('父组件修改 name')
        .onClick(() => {
          this.person.name = '李四'  // ✅ 触发更新
        })

      Divider()

      // 2. @Prop:单向传递(值传递)
      PropChild({ person: this.person })

      Divider()

      // 3. @Link:双向绑定(需要 $ 语法)
      LinkChild({ count: $count })

      Divider()

      // 4. @ObjectLink:双向绑定(深度观察)
      ObjectLinkChild({ person: this.person })
    }
  }
}

@Component
struct PropChild {
  @Prop person: Person

  build() {
    Column() {
      Text(`@Prop 子组件: ${this.person.name}`)

      Button('修改 name (无效)')
        .onClick(() => {
          // ❌ @Prop 是只读的,修改不会影响父组件
          this.person.name = '王五'
        })
    }
  }
}

@Component
struct LinkChild {
  @Link count: number

  build() {
    Column() {
      Text(`@Link 子组件: ${this.count}`)

      Button('修改 count')
        .onClick(() => {
          this.count++  // ✅ 父子双向同步
        })
    }
  }
}

@Component
struct ObjectLinkChild {
  @ObjectLink person: Person

  build() {
    Column() {
      Text(`@ObjectLink 子组件: ${this.person.name}, ${this.person.age}`)

      Button('修改 age')
        .onClick(() => {
          this.person.age++  // ✅ 父子双向同步(深度观察)
        })
    }
  }
}

🎯 六、最佳实践

6.1 什么时候使用 @ObjectLink?

✅ 使用场景:

  1. 嵌套对象的深度观察

    • 需要观察对象内部属性的变化
    • 对象有多层嵌套结构
  2. 复杂对象的父子同步

    • 需要父子组件同步修改对象
    • 对象有多个属性需要同步
  3. 列表中的对象

    • 数组中的对象需要单独传递给子组件
    • 需要修改数组中对象的属性
  4. 表单数据管理

    • 表单有多个字段
    • 需要实时预览表单数据

❌ 不适用场景:

  1. 简单类型数据

    • 数字、字符串、布尔值等:使用 @Prop@Link
  2. 只读数据

    • 子组件不需要修改数据:使用 @Prop
  3. 单层对象

    • 对象只有一层属性:使用 @Link

6.2 代码组织建议

// ========== models.ts ==========
// 统一管理所有 @Observed 类

@Observed
export class User {
  id: number
  name: string
  profile: UserProfile

  constructor(id: number, name: string) {
    this.id = id
    this.name = name
    this.profile = new UserProfile()
  }
}

@Observed
export class UserProfile {
  avatar: string = ''
  bio: string = ''
  age: number = 18
}

// ========== ParentPage.ets ==========
import { User } from './models'

@Entry
@Component
struct ParentPage {
  @State user: User = new User(1, '张三')

  build() {
    Column() {
      ChildComponent({ user: this.user })
    }
  }
}

// ========== ChildComponent.ets ==========
import { User } from './models'

@Component
export struct ChildComponent {
  @ObjectLink user: User

  build() {
    Column() {
      Text(this.user.name)
    }
  }
}

🎓 七、快速记忆

核心要点

  1. @Observed:修饰,让类的实例可被深度观察
  2. @ObjectLink:修饰变量,接收可观察对象
  3. 必须配合使用@Observed + @ObjectLink
  4. 解决问题:@State 只能观察第一层,@ObjectLink 可以深度观察
  5. 双向同步:子组件修改对象,父组件也会同步更新

使用步骤

1. 定义类,加 @Observed 装饰器
   ↓
2. 父组件用 @State 持有对象
   ↓
3. 传递对象给子组件
   ↓
4. 子组件用 @ObjectLink 接收
   ↓
5. 任意组件修改对象属性,都会触发双向更新

记忆口诀

Observed 修饰类,对象可观察
ObjectLink 接收它,深度来同步
父子共享引用,双向自动更新
嵌套对象必备,表单列表首选

📌 八、总结

何时使用?

场景 推荐方案
简单类型(数字、字符串) @Prop / @Link
对象的第一层属性 @Link
对象的嵌套属性(深度观察) @ObjectLink
列表中的对象 @ObjectLink
复杂表单数据 @ObjectLink
需要深度观察的购物车、用户信息 @ObjectLink

核心优势

  1. 深度观察:解决 @State/@Link 只能观察第一层的问题
  2. 双向同步:父子组件自动同步,无需手动处理
  3. 简洁语法:直接传递对象,不需要 $ 语法
  4. 性能优化:只有变化的属性才会触发更新

完! 🚀

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐