欢迎光临
我们一直在努力

状态管理进阶:@Prop、@ObjectLink与@Observed深度联动(21)

在 HarmonyOS 的 ArkUI 状态管理(V1)中,@Prop、@ObjectLink 与 @Observed 的组合是处理复杂数据流、实现高性能 UI 渲染的核心。要深度联动这三个装饰器,首先需要明确它们各自的职责与边界,然后掌握在不同业务场景下的最佳实践。

一、 核心概念与联动机制

  • @Observed(类装饰器):它的核心作用是赋予类实例“可被深度观察”的能力。默认情况下,状态管理装饰器(如 @State、@Prop)只能观察到对象第一层属性的变化。当一个类被 @Observed 装饰后,框架会为其创建不透明的代理对象,从而能够拦截并观察到嵌套对象(第二层及以下)的属性变化。
  • @ObjectLink(变量装饰器):它是 @Observed 的“搭档”,专门用于在子组件中接收 @Observed 装饰的类实例,并与父组件建立双向数据同步。它相当于一个指向父组件数据源的指针,修改其属性会直接修改父组件的数据,且自身不允许被整体重新赋值。
  • @Prop(变量装饰器):用于在父子组件间建立单向数据同步。子组件可以修改 @Prop 变量,但修改不会同步回父组件。当父组件数据源更新时,子组件的 @Prop 会被覆盖。
1、 深度联动实例:@Observed + @ObjectLink(双向嵌套同步)

业务场景:父组件持有一个复杂的嵌套对象,子组件需要直接修改该对象深层的属性,并且修改结果需要实时同步回父组件。

// 1. 必须使用 @Observed 装饰嵌套的类,赋予其被深度观察的能力
@Observed
class InnerInfo {
public detail: string;
constructor(detail: string) {
this.detail = detail;
}
}

@Observed
class OuterModel {
public name: string;
public inner: InnerInfo;
constructor(name: string, inner: InnerInfo) {
this.name = name;
this.inner = inner;
}
}

// 2. 子组件使用 @ObjectLink 接收,建立双向同步
@Component
struct ChildComponent {
// 接收父组件传入的 @Observed 类实例
@ObjectLink model: OuterModel;

build() {
Column({ space: 10 }) {
Text(`Name: ${this.model.name}`)
// 能够观察到第二层嵌套对象 inner 的属性变化
Text(`Detail: ${this.model.inner.detail}`)

Button('修改深层属性')
.onClick(() => {
// 直接修改深层属性,父组件也会同步更新
this.model.inner.detail = 'Updated by Child';
})
}
}
}

// 3. 父组件持有状态并传递给子组件
@Entry
@Component
struct ParentComponent {
@State myModel: OuterModel = new OuterModel('Parent', new InnerInfo('Initial Detail'));

build() {
Column({ space: 20 }) {
Text(`Parent Detail: ${this.myModel.inner.detail}`)

// 传递引用,不会发生深拷贝
ChildComponent({ model: this.myModel })
}
}
}

2、 性能对比实例:@Prop 的深拷贝开销 vs @ObjectLink 的零拷贝

业务场景:子组件仅用于展示列表项数据,不需要修改原始数据源。

@Observed
class Product {
public title: string;
constructor(title: string) {
this.title = title;
}
}

// 【反例】使用 @Prop 传递复杂对象(不推荐)
@Component
struct PropChild {
// @Prop 会对 Product 进行深拷贝,如果列表有100项,就会触发100次深拷贝,导致严重卡顿
@Prop product: Product;

build() {
Text(this.product.title)
}
}

// 【正例】使用 @ObjectLink 传递复杂对象(推荐)
@Component
struct ObjectLinkChild {
// 仅传递内存引用,无深拷贝开销,性能极佳
@ObjectLink product: Product;

build() {
Text(this.product.title)
}
}

@Entry
@Component
struct ListParent {
@State products: Product[] = [new Product('Phone'), new Product('Tablet')];

build() {
List() {
ForEach(this.products, (item: Product) => {
ListItem() {
// 优先使用 ObjectLinkChild
ObjectLinkChild({ product: item })
}
})
}
}
}

3、 单向同步实例:@Prop + @Observed(本地状态副本)

业务场景:父组件下发一个嵌套对象,子组件需要在本地对其进行修改(如展开/折叠状态、草稿编辑),但不希望影响父组件的原始数据。

@Observed
class Draft {
public content: string;
constructor(content: string) {
this.content = content;
}
}

@Component
struct DraftEditor {
// @Prop 会深拷贝一份 Draft 对象,子组件拥有独立副本
@Prop draft: Draft;

build() {
Column() {
Text(`Local Content: ${this.draft.content}`)
Button('本地修改')
.onClick(() => {
// 修改仅停留在当前子组件,不会同步回父组件
this.draft.content += ' (Edited Locally)';
})
}
}
}

@Entry
@Component
struct EditorParent {
@State originalDraft: Draft = new Draft('Original Text');

build() {
Column({ space: 20 }) {
Text(`Parent Content: ${this.originalDraft.content}`)
Button('父组件更新')
.onClick(() => {
// 父组件更新时,会覆盖子组件 @Prop 本地的所有修改
this.originalDraft.content = 'Reset by Parent';
})

DraftEditor({ draft: this.originalDraft })
}
}
}

二、 深度联动:@Prop 与 @Observed 的结合

虽然 @ObjectLink 是 @Observed 的最佳搭档,但 @Prop 同样可以与 @Observed 联动,实现嵌套对象的单向同步。

  • 观察能力:当 @Prop 装饰的变量类型是被 @Observed 装饰的 class 时,它可以观察到该 class 第一层属性的变化。如果嵌套的属性也是 class,同样需要被 @Observed 装饰才能被观察到。
  • 单向同步特性:与 @ObjectLink 不同,@Prop 会在本地拷贝一份数据源。子组件对 @Prop 嵌套对象属性的修改是允许的,但不会同步给父组件。如果父组件的数据源发生变化,子组件本地的修改将被覆盖。

业务场景:父组件下发一个嵌套对象,子组件需要在本地修改该对象的深层属性(如展开/折叠状态、草稿编辑),但不希望影响父组件的原始数据。

// 1. 嵌套类必须被 @Observed 装饰,否则深层属性变化无法被 @Prop 观察到
@Observed
class InnerInfo {
public detail: string;
constructor(detail: string) {
this.detail = detail;
}
}

@Observed
class OuterModel {
public name: string;
public inner: InnerInfo;
constructor(name: string, inner: InnerInfo) {
this.name = name;
this.inner = inner;
}
}

// 2. 子组件使用 @Prop 接收,建立单向同步
@Component
struct ChildComponent {
// @Prop 会深拷贝 OuterModel 及其嵌套对象,子组件拥有独立副本
@Prop model: OuterModel;

build() {
Column({ space: 10 }) {
Text(`Local Name: ${this.model.name}`)
Text(`Local Detail: ${this.model.inner.detail}`)

Button('本地修改深层属性')
.onClick(() => {
// 修改仅停留在当前子组件,不会同步回父组件
this.model.inner.detail = 'Edited by Child';
})
}
}
}

// 3. 父组件持有状态并传递给子组件
@Entry
@Component
struct ParentComponent {
@State myModel: OuterModel = new OuterModel('Parent', new InnerInfo('Initial Detail'));

build() {
Column({ space: 20 }) {
Text(`Parent Detail: ${this.myModel.inner.detail}`)

Button('父组件重置数据')
.onClick(() => {
// 父组件更新时,会覆盖子组件 @Prop 本地的所有修改
this.myModel.inner.detail = 'Reset by Parent';
})

ChildComponent({ model: this.myModel })
}
}
}

三、 性能避坑:@Prop 与 @ObjectLink 的抉择

在实际开发中,选择 @Prop 还是 @ObjectLink 对应用的性能有显著影响。

  • @Prop 的深拷贝开销:当使用 @Prop 传递复杂对象(特别是被 @Observed 装饰的类)时,框架会执行深拷贝(Deep Copy)。如果嵌套层级深或数据量大,这会显著增加组件的创建时间并带来性能开销。
  • 最佳实践:
    • 子组件无需修改状态时:如果子组件仅仅是展示数据,不需要在本地修改该状态变量,强烈建议优先使用 @ObjectLink。因为它不会深拷贝数据,仅传递引用,性能远优于 @Prop。
    • 子组件需要本地修改状态时:如果子组件需要修改传入的数据,且不希望影响父组件,此时才使用 @Prop。但需注意,在组件复用场景下,建议 @Prop 深度嵌套的数据不要超过 5 层,否则深拷贝和垃圾回收(GC)会引起严重的性能问题。若超过 5 层,应考虑重构数据结构或使用 @ObjectLink。

业务场景:在列表中渲染大量复杂数据项(如朋友圈动态、商品列表)。

@Observed
class FriendMoment {
public id: string;
public userName: string;
public text: string;
constructor(id: string, userName: string, text: string) {
this.id = id;
this.userName = userName;
this.text = text;
}
}

// 【反例】使用 @Prop 传递复杂对象(性能杀手)
@Component
struct MomentPropItem {
// 每次组件创建时,都会对 FriendMoment 进行深拷贝
// 如果列表有 100 条数据,就会触发 100 次深拷贝,导致列表滑动严重掉帧
@Prop moment: FriendMoment;

build() {
Text(`${this.moment.userName}: ${this.moment.text}`)
}
}

// 【正例】使用 @ObjectLink 传递复杂对象(高性能)
@Component
struct MomentObjectLinkItem {
// 仅传递内存引用(指针),无深拷贝开销
// 父子组件共享同一份数据,创建时间和内存消耗极低
@ObjectLink moment: FriendMoment;

build() {
Text(`${this.moment.userName}: ${this.moment.text}`)
}
}

@Entry
@Component
struct MomentListPage {
@State moments: FriendMoment[] = [
new FriendMoment('1', 'Alice', 'Hello HarmonyOS'),
new FriendMoment('2', 'Bob', 'ArkUI is great')
];

build() {
List() {
ForEach(this.moments, (item: FriendMoment) => {
ListItem() {
// 强烈建议使用正例写法
MomentObjectLinkItem({ moment: item })
}
}, (item: FriendMoment) => item.id) // 必须提供唯一 key
}
}
}

四、 联动实战指南

1、 对象数组与 ForEach 的深度联动

在实际开发中,最常见的复杂数据结构是“对象数组”(如商品列表、聊天记录)。如果直接传递数组项,框架默认只能观察到数组项的整体替换,无法观察到数组项内部属性的变化。

最佳实践:将数组项定义为 @Observed 类,在 ForEach 中将其作为 @ObjectLink 传递给子组件。

@Observed
class CartItem {
public id: number;
public count: number;
constructor(id: number, count: number) {
this.id = id;
this.count = count;
}
}

@Component
struct CartItemComponent {
// 接收数组中的具体项
@ObjectLink item: CartItem;

build() {
Row() {
Text(`商品ID: ${this.item.id}`)
Text(`数量: ${this.item.count}`)
Button('+').onClick(() => {
// 直接修改深层属性,UI 会精准刷新,且父组件的数组也会同步更新
this.item.count++;
})
}
}
}

@Entry
@Component
struct CartPage {
@State cartList: CartItem[] = [
new CartItem(1001, 1),
new CartItem(1002, 2)
];

build() {
List() {
ForEach(this.cartList, (item: CartItem) => {
ListItem() {
CartItemComponent({ item: item })
}
}, (item: CartItem) => item.id.toString()) // 必须提供唯一 key
}
}
}

2、 极限嵌套:多层 Class 的观察链传递

当业务模型极其复杂(例如:Order 包含 User,User 又包含 Address)时,普通的 @State 或 @Prop 只能观察到第一层(Order)的变化。要实现深层观察,每一层嵌套的 Class 都必须被 @Observed 装饰,并在组件间通过 @ObjectLink 逐层传递。

@Observed
class Address {
public city: string = 'Hangzhou';
}

@Observed
class User {
public name: string = 'Tom';
public address: Address = new Address();
}

@Entry
@Component
struct ProfilePage {
@State user: User = new User();

build() {
Column() {
// 将第二层嵌套对象直接传给子组件
AddressComponent({ address: this.user.address })
}
}
}

@Component
struct AddressComponent {
@ObjectLink address: Address;

build() {
Text(`当前城市: ${this.address.city}`)
.onClick(() => {
// 修改第三层属性,依然能触发 UI 刷新
this.address.city = 'Beijing';
})
}
}

3、 架构演进:V1 与 V2 状态管理的混用与兼容

随着 HarmonyOS API 版本的升级,官方推出了性能更优、支持深层观察的状态管理 V2(@ObservedV2 + @Param)。在实际工程中,老代码(V1)与新代码(V2)共存是常态。

混用避坑指南:
当 V2 组件需要将嵌套对象传递给 V1 的 @ObjectLink 时,由于两者的底层观察机制不同,直接传递会导致不刷新。必须使用 UIUtils.enableV2Compatibility 和 UIUtils.makeV1Observed 进行桥接转换。

import { UIUtils } from '@kit.ArkUI';

// V1 的普通类
class V1Model {
public name: string = 'Tom';
}

// V2 父组件
@ComponentV2
struct ParentV2 {
// 必须调用工具方法进行兼容转换
@Local v1Model: V1Model = UIUtils.enableV2Compatibility(
UIUtils.makeV1Observed(new V1Model())
);

build() {
Column() {
// 安全地传递给 V1 子组件
ChildV1({ model: this.v1Model })
}
}
}

// V1 子组件
@Component
struct ChildV1 {
@ObjectLink model: V1Model;

build() {
Text(this.model.name).onClick(() => {
this.model.name += '!'; // 正常触发刷新
})
}
}

  • 明确数据流向:
    • 需要父组件和子组件双向联动(如表单编辑、复杂状态共享):使用 @Observed + @ObjectLink。
    • 需要父组件向子组件单向传递,且子组件有独立的本地状态副本(如列表项的展开/折叠状态):使用 @Observed + @Prop。
  • 规范类定义:
    • 所有需要被 @ObjectLink 接收,或需要被深度观察的类,必须加上 @Observed 装饰器。
    • 如果类内部嵌套了其他复杂对象,嵌套的类也必须加上 @Observed,否则内层属性的变化无法触发 UI 刷新。
  • 遵守赋值规则:
    • @ObjectLink 装饰的变量是只读的,禁止整体赋值(如 this.objLink = newObj),只能修改其内部属性(如 this.objLink.name = 'newName')。
    • @Prop 装饰的变量允许本地赋值,但需知晓父组件更新时会覆盖本地修改。
  • 通过合理联动这三个装饰器,开发者可以构建出数据流向清晰、UI 刷新精准且性能优异的应用架构。

    如果项目是基于最新的 API 开发,且面临极其复杂的嵌套数据流,建议直接使用 状态管理 V2。V2 采用了直接数据观察机制,彻底解决了 V1 的痛点:

  • 天然支持深层观察:在 V2 中,使用 @ObservedV2 装饰类,并使用 @Trace 装饰需要被观察的属性。无论是嵌套多深的对象,修改其属性都能自动触发视图更新,无需再使用 @ObjectLink 逐层传递。
  • 组件输入输出显式化:V2 引入了 @Param(单向)和 @Event(双向回调),替代了 V1 中容易混淆的 @Prop 和 @Link,使组件化设计更加清晰。
  • 消除冗余渲染:V2 支持按属性级别更新,避免了 V1 中因修改嵌套属性而导致整个父组件或列表重渲染的性能问题。
  • 赞(0)
    未经允许不得转载:171主机测评 » 状态管理进阶:@Prop、@ObjectLink与@Observed深度联动(21)
    分享到: 更多 (0)

    评论 抢沙发

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