第一章 引言:为什么需要@Observed与@ObjectLink?

在鸿蒙ArkUI应用开发中,状态管理是构建响应式用户界面的基石。开发者常常面临这样的困境:数据模型并非扁平的单层结构,而是包含对象嵌套、数组嵌套的复杂树状结构。然而,@State@Link@Prop等V1版本的状态管理装饰器仅能观察数据第一层属性的变化。当数据模型深度超过一层时,其局限性便暴露无遗。

例如,一个典型的用户数据模型包含用户信息及其所属部门信息:

typescript

class Department {
  name: string;
  location: string;
}

class User {
  name: string;
  dept: Department; // 嵌套对象
}

当你在UI中展示用户所在部门名称,并尝试通过点击事件修改user.dept.name = "新部门"时,若仅使用@State装饰user对象,UI将不会刷新。这是因为@State仅能观测到user变量本身被重新赋值,而无法观测到其内部dept属性的变化。

@Observed@ObjectLink正是为解决这一深层嵌套数据观察问题而生的组合。@Observed用于标记一个类为可观察对象,而@ObjectLink在子组件中接收并绑定这个被观察对象的实例,建立双向数据同步,并确保嵌套属性的变化能被精准感知并触发UI更新。

第二章 @Observed装饰器:代理机制的起点

2.1 装饰器的本质:动态代理注入

@Observed的核心能力并非仅仅是为类打上一个元数据标记。其本质是在类的构造函数中动态注入逻辑,返回一个ES6 Proxy代理对象,从而实现对属性读写操作的拦截。

当ArkUI的编译器处理如下代码时:

typescript

@Observed
class Department {
  name: string;
  location: string;
}

编译器会将其转换为类似如下形式的代码:

typescript

class Department {
  // ... 原始类定义
  constructor(...args) {
    // 1. 调用原始构造函数
    // 2. 检查是否已被代理
    // 3. 若未代理,则创建并返回一个Proxy对象
    return ObservedObject.createNewInternal(this, undefined);
  }
}

这个createNewInternal方法会根据传入对象的类型(普通对象、数组、Map/Set、Date),创建带有不同处理器(Handler) 的代理对象。

2.2 四种专用处理器(Handler)的分工

为了精确处理不同类型数据的变化,底层实现了四种专用的处理器,它们继承自一个基础的SubscribableHandler基类:

处理器类 适用类型 核心职责与拦截目标
SubscribableHandler 普通类对象 拦截属性的getset操作。set时触发变化通知。
SubscribableArrayHandler 数组 (Array) get拦截中包装pushpopsplice等变异方法,使其能触发通知。
SubscribableMapSetHandler Map 和 Set 拦截并包装setadddeleteclear等变异方法,并处理内部槽问题。
SubscribableDateHandler Date 对象 拦截并包装setFullYearsetMonthsetDate等设值方法。

关键点@Observed无法观察WeakMapWeakSetPromise等特殊类型属性的变化,因为底层没有为其实现对应的处理器。同时,@Observed会改变类的原型链,应避免与其他类装饰器混用。

2.3 setter陷阱的完整执行流程

当对@Observed实例的属性进行赋值(this.obj.prop = newValue)时,代理的set陷阱会被触发。其执行逻辑如下:

  1. 拦截与去重:接收目标对象、属性名和新值。若属性值是Symbol或新旧值相同,则直接返回,不进行后续操作。

  2. 执行赋值:调用Reflect.set将新值写入原始对象。

  3. 依赖通知:遍历该属性的订阅者列表,调用notifyObjectPropertyHasChanged方法,通知所有依赖此属性的组件数据已变化。

  4. 优化模式:支持@Track装饰器进行精细控制。若类中使用了@Trackset陷阱仅会在被@Track标记的属性变化时触发通知,否则忽略变化。若未使用@Track,则任何属性变化都会触发通知。

第三章 @ObjectLink装饰器:订阅链路的构建

3.1 双向同步的建立

@ObjectLink装饰器在子组件中发挥作用。它的核心职责是接收父组件传递过来的、被@Observed装饰的类实例,并建立从子组件到数据源的双向同步链路

从语义上看,@ObjectLink可以被视为指向父组件中原始数据源的指针,而非数据的副本。这正是它与@Prop(单向同步,本地拷贝)和@Link(双向同步,但无法观察深层变化)的本质区别。

3.2 初始渲染时的注册行为

在初始渲染阶段,@ObjectLink的底层行为如下:

  1. 初始化:父组件通过@ObjectLink装饰的属性将@Observed实例传递给子组件。

  2. 自我注册@ObjectLink包装类会将自己(即这个特定的@ObjectLink实例)注册到它所引用的@Observed实例的订阅者列表中。这个注册行为意味着,@ObjectLink@Observed实例提供了自身的引用,以便被观察对象在属性变化时能通知到它。

这一注册过程是依赖收集的关键一步。通过注册,@Observed实例持有了一份所有依赖它的@ObjectLink列表,为后续的精准通知奠定了基础。

3.3 关键约束:只读引用,不可赋值

@ObjectLink有一个核心约束:它所装饰的变量本身是只读的,不允许被重新赋值

typescript

// 正确:修改对象的属性
this.objLink.a = 5; 

// 错误:不能对@ObjectLink变量本身赋值
// this.objLink = new MyClass(); // 这会导致同步链断裂

如果对@ObjectLink变量重新赋值,将重置其引用,切断与原始数据源的同步链路,变化将无法再传回父组件。

第四章 通信流程全链路:从用户操作到UI刷新

综合前两章的分析,可以梳理出从用户交互到UI更新的完整底层通信流程:

第一步:初始渲染与依赖收集

  1. 父组件通过@State等装饰器持有根数据。

  2. 父组件在build函数中,将@Observed实例的一部分(如this.user.bag)传递给子组件。

  3. 子组件通过@ObjectLink接收该实例。@ObjectLink的包装类在其构造函数或初始化逻辑中,会调用@Observed实例上的一个特定Symbol方法(如SUBSCRIBE),将自身引用添加到@Observed实例的订阅者列表中。至此,依赖关系正式建立。

第二步:数据变化拦截

  1. 用户操作触发一个事件(如点击按钮),该事件回调中修改了@ObjectLink所指向的对象的属性(例如this.bag.size += 1)。

  2. 由于该对象是@Observed的代理对象,属性的set陷阱被拦截。

第三步:通知分发

  1. 代理的set陷阱检测到值变化后,会获取该属性的订阅者列表。

  2. 列表中的每个元素都是一个@ObjectLink包装类实例。

  3. @Observed实例会遍历该列表,并调用每个@ObjectLink包装类的notify或类似方法,告知数据变化。

第四步:UI组件更新

  1. 收到通知的@ObjectLink包装类,会将其关联的UI组件标记为需要重新渲染(dirty)。

  2. ArkUI框架的下一个渲染周期,会重新执行该组件的build函数,读取@ObjectLink指向的最新数据。

  3. 最终,UI被刷新,展示新的值。

下图可以直观地展示这一闭环:

第五章 实战案例:任务管理系统中的深度监听

以一个项目管理案例说明其应用。该案例包含Project -> Task -> SubTask三层嵌套结构。

typescript

@Observed
class SubTask {
  completed: boolean = false;
  // ...
}

@Observed
class Task {
  subTasks: SubTask[] = [];
  // ...
  toggleSubTask(index: number) {
    this.subTasks[index].completed = !this.subTasks[index].completed;
    // 更新进度...
  }
}

@Observed
class Project {
  tasks: Task[] = [];
}

UI结构

  • ProjectManagement(顶层):持有@State project: Project

  • TaskItem(中间层):通过@ObjectLink task: Task接收单个任务。

  • SubTaskItem(叶子层):通过@ObjectLink subTask: SubTask接收单个子任务。

交互与数据流

  1. 用户在SubTaskItem中点击复选框,调用parentTask.toggleSubTask(index)

  2. 该方法修改了SubTask实例的completed属性。

  3. 由于SubTask@Observed装饰,其代理的set陷阱被触发。

  4. 所有依赖该SubTask实例的@ObjectLink(即SubTaskItem组件)收到通知,UI刷新。

  5. 随后,TaskupdateProgress()方法被调用,修改了Taskprogress属性。同样,依赖此TaskTaskItem组件也因@ObjectLink的监听而刷新进度条。

  6. 最终,ProjectgetOverallProgress()方法基于tasks数组计算总进度,顶层组件重新渲染,更新总进度显示。

关键点:变化沿着SubTask -> Task -> Project的链路逐级精准冒泡,每一层仅刷新受影响的部分,无需全量刷新整个页面,保证了性能。

第六章 V1方案的局限性与V2演进

@Observed@ObjectLink的V1方案虽然解决了深层监听问题,但也带来了新的挑战,推动了V2状态管理方案的诞生。

V1方案的核心痛点在于其“传递性”。要实现深度监听,每一层嵌套的数据结构都必须用@Observed装饰,并且每一层的UI组件都必须通过@ObjectLink来接收数据。这导致以下问题:

  1. 代码冗余:对于一个7-8层的嵌套对象,需要创建7-8层对应的@ObjectLink组件来传递数据,模板代码激增。

  2. 侵入性强:每一层组件都必须感知自己接收的是@ObjectLink,而非普通的@State@Prop,增加了组件间的耦合度。

  3. 学习曲线陡峭:开发者需要深刻理解“只读引用”、“不可赋值”等概念,否则容易出错。

V2方案(@ObservedV2与@Trace)的改进
V2方案旨在解决上述问题。其核心思路是将“观测能力”与“数据传递”解耦。

  • 在V2中,只需在数据类的需要变化的属性上标记@Trace,而不需要将每一层都通过@ObjectLink传递。

  • 组件依然可以使用@State或普通的@Local等装饰器持有数据,只要数据对象本身或其属性被@ObservedV2@Trace标记,任何深度的属性变化都能直接触发UI刷新,无需层层传递@ObjectLink

这极大简化了代码,并降低了理解成本。不过,V1和V2目前可以混用,但需要注意规则(如@State不能直接与@ObservedV2混用)。新项目推荐直接使用V2方案。

第七章 总结

@Observed@ObjectLink的底层通信流程,本质上是基于ES6 Proxy的代理模式发布-订阅模式的优雅结合。@Observed通过代理拦截数据变化,@ObjectLink通过注册建立依赖关系,二者协同工作,精准地将数据层的深层变化传递到UI层,实现了高效的响应式更新。

对比维度 @Observed @ObjectLink
装饰目标 类 (Class) 组件的状态变量
核心机制 创建ES6 Proxy代理,拦截属性读写 接收代理实例,注册为订阅者
主要职责 标记可观察类,拦截变化并通知订阅者 建立双向同步链路,响应变化并触发UI更新
关键约束 改变原型链,避免与其他类装饰器混用 变量只读,不可重新赋值
组合关系 必须与@ObjectLink@Prop配合使用才有效 必须接收被@Observed装饰的类实例
Logo

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

更多推荐