@Link 还是 @ObjectLink?HarmonyOS 状态管理深层差异一次捋清

写 ArkUI 组件化时,十有八九会碰到这个让人犯迷糊的问题:父子组件要共享状态,到底用 @Link 还是 @ObjectLink 初看都能"传进去",改了都能刷 UI,可一旦涉及对象嵌套、数组项、能不能整体替换引用——差别就龇出来了。用错倒也不一定崩,但会带来隐蔽的"不刷新"Bug,查起来相当搞心态。


一、 先建立直觉:响应链是怎么连上的?

ArkUI(V1) 的状态管理本质是一套带版本号的脏标记(Dirty Flag)+ 观察者模式@State 是源头,@Link / @ObjectLink 是下游订阅者。

  • @Link:在编译期,框架在父 VNode 和子 VNode 之间建立一条双向绑定通道(Binding)。子组件对 @Link 变量重新赋值(this.count = newValue)会反向写回父组件的 @State,触发父→子整链重算。
  • @ObjectLink + @Observed@Observed 修饰的类在运行时被套了一层 Proxy/DefineProperty 代理。子组件的 @ObjectLink 持有该代理对象引用,当代理捕获到属性 set 操作时标记脏节点。@ObjectLink 不允许整体重新赋值(不能 = new Obj()),它只观测"所指向的这个实例内部的变化"。

用一张彩色分区流程图把关系画清楚:

@Observed 代理层

子组件 (用 @ObjectLink)

子组件 (用 @Link)

父组件 (Parent)

@Link 双向绑定通道

传入同一个 Observed 实例

持有引用

属性 set 触发 UI 局部刷新

@State data: T
@State obj: ObservedClass

@Link data: T
✅ 可重新赋值 → 写回父 @State
✅ 基本类型 / 整个对象替换

@ObjectLink obj: ObservedClass
❌ 不能重新赋值 (= new)
✅ 可修改 obj.prop → 触发代理通知刷新

对 ObservedClass 属性
劫持 set/get
属性变更 → markDirty()

一句话先记住:@Link 管"指向谁"(可替换),@ObjectLink 管"这个对象里面变了啥"(深层属性观测)。


二、 @Link 实战——基本类型与整体对象替换

最常用的场景:计数器、开关态、甚至把一个整体对象传下去并允许子组件换成一个新对象同步回父。

// ParentComponent.ets
@Component
struct ParentComponent {
  @State count: number = 0;
  @State user: { name: string; age: number } = { name: 'Alice', age: 18 };

  build() {
    Column({ space: 12 }) {
      Text(`父组件 count = ${this.count}`).fontSize(18)
      Text(`父组件 user = ${this.user.name}, ${this.user.age}`).fontSize(14)

      Divider().margin({ top: 8, bottom: 8 })

      // 传基本类型和对象整体
      ChildWithLink({ count: this.count, user: this.user })
    }
    .padding(20)
  }
}

@Component
struct ChildWithLink {
  // 核心:@Link 双向绑定
  @Link count: number;
  @Link user: { name: string; age: number };

  build() {
    Column({ space: 10 }) {
      Button('子组件 count +1')
        .onClick(() => {
          this.count++;  // ✅ 写回父组件的 @State
        })

      Button('子组件 换新 User 对象')
        .onClick(() => {
          // ✅ @Link 允许整体替换,父也会同步
          this.user = { name: 'Bob', age: 20 };
        })

      Button('子组件 改 user.age(属性赋值)')
        .onClick(() => {
          // ⚠️ 对 @Link 中的对象做属性修改——API 9+ 多数情况能刷新
          // 但若 user 是数组项或嵌套更深,不保证观测!推荐用 @ObjectLink+@Observed
          this.user.age++;
        })
    }
    .padding(12)
    .backgroundColor('#EEEEEE')
    .borderRadius(8)
  }
}

跑一下你会看到:点 “+1” 父数字跟着变;点"换新 User"父引用的对象也换了——这就是 @Link 的双向替换能力。


三、 @ObjectLink + @Observed 实战——观测对象内部属性变化

当你的数据模型是个类(尤其嵌套在数组、@State list: ObservedClass[] 场景),@Link 观测不到数组内某个对象属性变更(只感知"数组引用变了")。这时正当用法是 @Observed + @ObjectLink

// models/Student.ets
@Observed  // ← 关键:让此类支持属性代理观测
export class Student {
  // API 11+ 推荐显式标 @Track 减少不必要刷新;旧版不标也会观测顶层属性
  name: string;
  score: number;

  constructor(name: string, score: number) {
    this.name = name;
    this.score = score;
  }
}
// ParentObserved.ets
import { Student } from './models/Student';

@Component
struct ParentObserved {
  @State students: Student[] = [
    new Student('张三', 80),
    new Student('李四', 92)
  ];

  build() {
    Column({ space: 12 }) {
      Text('学生列表(父)').fontSize(18).fontWeight(FontWeight.Bold)

      ForEach(this.students,
        (item: Student, idx: number) => {
          // 把 Observed 实例传给子组件的 @ObjectLink
          StudentCard({ student: item })
        },
        (item: Student) => item.name
      )

      Button('父:整体替换第一个学生')
        .onClick(() => {
          // 替换数组项引用 → ForEach 和 @ObjectLink 均感知
          this.students[0] = new Student('王五', 88);
        })
    }
    .padding(20)
  }
}

@Component
struct StudentCard {
  // 核心:@ObjectLink 持有 Observed 实例引用
  @ObjectLink student: Student;

  build() {
    Row({ space: 12 }) {
      Text(`${this.student.name} - 分数: ${this.student.score}`)

      Button('+5分')
        .onClick(() => {
          // ✅ 修改对象属性 → @Observed 代理捕获 set → UI 刷新
          this.student.score += 5;
        })

      // ❌ 下面这行会编译报错或运行时无效(Assigning to '@ObjectLink' is not allowed)
      // this.student = new Student('xx', 0);
    }
    .padding(8)
    .backgroundColor('#E8F5E9')
    .borderRadius(6)
  }
}

两个要点绷在心里:

  1. @ObjectLink 只能接收 @Observed 类的实例(或经 new 构造、或父 @State 持有的同一实例),传普通 JS 对象不会触发深层观测。
  2. 子组件不能给 @ObjectLink 变量整体赋值——它观测的是"这个实例内部",而不是"指向哪个实例"。

四、 选择决策:一张差异对照表

维度 @Link @ObjectLink + @Observed
典型用途 基本类型、整体对象替换、简单双向同步 观测 @Observed 类实例内部属性变化,尤指数组内对象
可整体重新赋值? ✅ 可以(this.linkVar = newVal 写回父) ❌ 达咩(编译/运行报错)
观测对象属性变更? ⚠️ 仅当对象是顶层 @State 且简单嵌套;数组项属性变更不保证触发 ✅ 代理劫持 set,精确触发局部刷新
传递要求 父用 $ 或直接绑 @State(对象需整体替换才刷新) 父必须传同一个 @Observed 类实例(通常来自 @State list: ObservedClass[]
适用层级 父子直接传递 多用于 List/ForEach 中渲染每个 Observed Item

经验法则(划重点):

  • 布尔/数字/字符串双向绑定 → @Link
  • 把整个对象换成另一个 → @Link
  • 修改对象或数组项内部字段且要响应式 → @Observed + @ObjectLink

五、 HarmonyOS 6(API 22)与状态管理 V2 适配前瞻

目前主流还是 ArkUI State Management V1@State/@Link/@ObjectLink/@Observed),它在 API 22 仍完全兼容。但 HarmonyOS 6 正式版预计会更积极推荐 V2(@ComponentV2 + @@State / @@Link / @@ObjectLink,这里有几点你要提前留意的:

1. V2 中等价概念对照

V1 V2(@ComponentV2 说明
@State @@State(即双 @ 号) 组件自有响应式状态
@Link @@Link 双向绑定,语义相同
@ObjectLink + @Observed @@ObjectLink + @@Observed(或搭配 @Trace 观测类实例内部变化
@Param / @Once / @Require V2 新增单向入参、一次性初始化等更严格契约

如果要在 API 22 尝鲜 V2:

@ComponentV2
struct StudentCardV2 {
  @@ObjectLink student: Student; // Student 仍需 @@Observed 或 @Observed(兼容)

  build() {
    Button(`V2 +5 → ${this.student.score}`)
      .onClick(() => { this.student.score += 5; })
  }
}

2. @Track 装饰器更受推崇(API 11+ 引入,API 22 默认推荐)

老代码不标 @Track@Observed 默认观测所有顶层属性赋值(可能造成多余刷新)。新规范建议:

@Observed
class Student {
  @Track name: string = '';
  @Track score: number = 0;
}

API 22 的开发工具链可能会对"未标 @Track 且有较多属性的 @Observed 类"给 lint 提示,提前养成习惯没坏处。

3. 废弃预警与兼容承诺

官方多次表态 V1 装饰器(@Link/@ObjectLink/@Observed在 API 22 仍保留且长期兼容,不会突然删除。但新工程模板可能默认勾选 V2(@ComponentV2),届时你需要判断团队接受度再决定跟不跟。

4. 数组/嵌套对象观测的强化

V2 引入的 @Trace(替代部分 @Observed 场景)和深层代理能力在 API 22 趋于完善,对二维数组、Map/Set 的观测比 V1 更直观。若你的业务重度依赖复杂嵌套状态(比如多维表格数据),可评估迁移 V2 的收益。


六、 避坑哦

  • @ObjectLink 接收非 @Observed 普通对象 → 不报错但不刷新,这是最高频的咨询问题。确认类上有 @Observed,且传入的是同一个实例。
  • ForEach 里用 @Link 传数组项对象 → 改属性不刷。正确做法是 ForEach(this.students, item => <StudentCard student={item} />) 搭配 @ObjectLink
  • @Link 子组件初始化必须用 $ 或绑父 @State 变量,写成 Child({ count: this.count })(少了 $)会变成单向传值,丢了双向绑定——编译不报错但怎么点都不变父组件。
  • 严格模式下的 @ObjectLink 赋值报错:若在重构时把原为 @Link 的字段改成 @ObjectLink,记得删掉所有试图 this.xxx = new XXX() 的代码,改为只改属性。

再来唠唠总结一下下

@Link@ObjectLink 不是二选一的优劣关系,而是各司其职——前者管引用与双向替换,后者管 Observed 对象内部变化。搞清"改指向"和"改内容"这个分野,大部分 ArkUI 状态困惑会瞬间消散。

写新代码时顺手判断:是基本类型/整体换对象 → @Link;是 @Observed 类实例属性观测(尤指 List Item)→ @ObjectLink。等 HarmonyOS 6 大面积铺开,再考虑往 V2(@@Link / @@ObjectLink)平滑迁移就好。

Logo

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

更多推荐