【学习目标】

  1. 完全掌握 @Observed 与 @ObjectLink 标准用法,能够实现多层级 class 对象的深度监听与双向同步
  2. 掌握 @Provide + @Consume 跨任意层级组件状态共享机制,实现无需逐层传参的双向数据同步。
  3. 能够根据业务场景(内部状态、父子单向、父子双向、嵌套深度、跨层级)正确选择对应的状态装饰器。
  4. 掌握 @Track 装饰器核心用法、约束及与其他装饰器的区别,实现 class 属性级精准UI刷新,优化渲染性能。

一、上节回顾与本节引入

在上一节中,我们系统学习了 ArkTS 状态管理 V1 版本的四大基础装饰器

  • @State:组件内部私有状态,数据变更驱动本组件刷新。
  • @Prop:父组件向子组件单向同步,使用深拷贝,子改父不变。
  • @Link:父组件与子组件双向同步,使用引用传递,一方改双方变。
  • @Watch:监听状态变量变更,执行自定义回调逻辑。

但是,这些基础装饰器在实际复杂业务开发中,存在两个非常明显的痛点与局限

1. 浅观察限制

@State、@Link、@Prop 都只对变量的第一层属性变更进行监听
当我们使用多层嵌套的 class 对象时:

  • 修改第一层属性:user.name = "xxx" → 可以刷新 UI
  • 修改嵌套属性:user.info.age = 20不会触发任何 UI 刷新

2. 层级传递繁琐

@Prop 和 @Link 只支持父子组件之间直接传递

为了解决这两个核心问题,在状态管理 V1 中提供了两组高阶装饰器

  1. @Observed + @ObjectLink:专门解决嵌套对象深度监听问题,让多层级属性修改也能正常刷新 UI。
  2. @Provide + @Consume:专门解决跨层级状态共享问题,实现祖先与后代组件直接双向同步。

二、工程结构

StateManagementDemo/
├── AppScope/
│   └── app.json5
├── entry/
│   ├── src/
│   │   ├── main/
│   │   │   ├── ets/
│   │   │   │   ├── entryability/
│   │   │   │   │   └── EntryAbility.ets
│   │   │   │   ├── model/
│   │   │   │   │   └── UserModel.ets       // 支持深度监听的模型类
│   │   │   │   ├── components/             // 分层自定义组件
│   │   │   │   │   ├── UserAgeItem.ets
│   │   │   │   │   └── UserInfoCard.ets
│   │   │   │   ├── pages/
│   │   │   │   │   ├── Index.ets           // 导航页面
│   │   │   │   │   ├── StatePage.ets
│   │   │   │   │   ├── PropPage.ets
│   │   │   │   │   ├── LinkPage.ets
│   │   │   │   │   ├── WatchPage.ets
│   │   │   │   │   ├── ObjectLinkPage.ets  // 深度监听示例页面
│   │   │   │   │   ├── ObjectLinkTwoDArrayPage.ets // 二维数组深度监听示例页面
│   │   │   │   │   ├── ProvideConsumePage.ets // 跨层级状态示例页面
│   │   │   │   │   ├── ComponentRenderPage.ets // 刷新冗余问题演示
│   │   │   │   │   └── TrackPage.ets        // @Track 示例页面
│   │   │   └── module.json5

2.2 数据模型更新(UserModel.ets)

// 嵌套的每一级 class,都必须添加 @Observed
@Observed
export class UserInfo {
  age: number;

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

@Observed
export class UserModel {
  name: string;
  info: UserInfo;

  constructor(name: string, info: UserInfo) {
    this.name = name;
    this.info = info;
  }
}
// 用于 @Track 演示的模型类
export class TrackDemo {
  @Track name: string;
  age: number;         // 未 @Track,禁止在 UI 中使用
  @Track gender: string;

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

2.3 导航页面更新(Index.ets)

import router from '@ohos.router';

interface PageItem {
  title: string;
  url: string;
}

@Entry
@Component
struct Index {
  private pageConfigs: PageItem[] = [
    { title: "1. @State 组件内部状态", url: 'pages/StatePage' },
    { title: "2. @Prop 父子单向同步", url: 'pages/PropPage' },
    { title: "3. @Link 父子双向同步", url: 'pages/LinkPage' },
    { title: "4. @Watch 状态变化监听", url: 'pages/WatchPage' },
    { title: "5. @Observed @ObjectLink 深度监听", url: 'pages/ObjectLinkPage' },
    { title: "6. 二维数组深度监听示例", url: 'pages/ObjectLinkTwoDArrayPage' },
    { title: "7. @Provide + @Consume 跨层级状态共享", url: 'pages/ProvideConsumePage' },
    { title: "8. 冗余刷新问题", url: 'pages/ComponentRenderPage' },
    { title: "9. @Track class 对象的属性装饰器", url: 'pages/TrackPage' }
  ];

  build() {
    Column({ space: 20 }) {
      Text("ArkTS 状态管理示例")
        .fontSize(30)
        .fontWeight(FontWeight.Bold);

      ForEach(this.pageConfigs, (item:PageItem) => {
        Button(item.title)
          .width('80%')
          .onClick(() => router.pushUrl({ url: item.url }));
      }, (item:PageItem) => item.url)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#f5f5f5');
  }
}

三、@Observed + @ObjectLink 深度监听

3.1 核心定义与作用

这组装饰器专门用于解决多层嵌套 class 对象无法监听问题。

@Observed
  • 装饰目标:class 类
  • 作用:标记这个类及其实例支持深度观察
  • 关键约束:
    • 如果一个 class 内部嵌套了另一个 class,那么两个 class 都必须添加 @Observed
    • 仅支持 class,不支持 number/string/boolean 等基础类型。
@ObjectLink
  • 装饰目标:子组件内部的变量
  • 作用:与被 @Observed 修饰的对象建立双向引用绑定,实现嵌套属性变更监听。
  • 核心规则:
    1. 只有为每一层嵌套对象提供独立的UI组件 + @ObjectLink绑定,才能直接修改嵌套属性(如 this.user.info.age += 1)并触发UI刷新;
    2. 如果未为嵌套层级提供独立UI组件,直接修改嵌套属性不会刷新UI,必须通过替换整个引用对象(如 this.user.info = new UserInfo(age+1))才能触发刷新。
  • 使用规则:
    1. 只能在子组件 @Component 中使用,不能在 @Entry 页面使用
    2. 禁止本地初始化,必须由父组件通过参数传递。
    3. 禁止对变量整体重新赋值,只允许修改对象内部属性。

3.2 分层组件示例(一层属性对应一层UI)

组件1:最内层 - 年龄修改组件(UserAgeItem.ets)
// components/UserAgeItem.ets
import { UserInfo } from '../model/UserModel';

@Component
export struct UserAgeItem {
  @ObjectLink userInfo: UserInfo;

  build() {
    Column({ space: 5 }) {
      Text(`当前年龄:${this.userInfo.age}`).fontSize(18);
      
      Button("年龄+1(内层组件修改)")
        .onClick(() => {
          this.userInfo.age += 1;
        });
    }
  }
}
组件2:中间层 - 用户信息组件(UserInfoCard.ets)
// components/UserInfoCard.ets
import { UserModel } from '../model/UserModel';
import { UserAgeItem } from './UserAgeItem';

@Component
export struct UserInfoCard {
  @ObjectLink user: UserModel;

  build() {
    Column({ space: 10 }) {
      Text(`用户名:${this.user.name}`).fontSize(20);
      
      Button("修改用户名")
        .onClick(() => {
          this.user.name = this.user.name + "✨";
        });

      Button("修改年龄(直接修改)")
        .onClick(() => {
          this.user.info.age += 1;
        });

      Button("修改年龄(替换引用)")
        .onClick(() => {
          this.user.info = new UserInfo(this.user.info.age + 1);
        });

      UserAgeItem({ userInfo: this.user.info });
    }
    .padding(20)
    .backgroundColor('#e0e0e0')
    .borderRadius(12);
  }
}
组件3:最外层 - 页面层(ObjectLinkPage.ets)
// pages/ObjectLinkPage.ets
import { UserInfo, UserModel } from '../model/UserModel';
import { UserInfoCard } from '../components/UserInfoCard';

@Entry
@Component
struct ObjectLinkPage {
  @State user: UserModel = new UserModel("鸿蒙开发者", new UserInfo(20));

  build() {
    Column({ space: 20 }) {
      Text("父组件 - 深度监听页面").fontSize(28).fontWeight(FontWeight.Bold);
      
      Text(`父组件姓名:${this.user.name}`).fontSize(22);
      Text(`父组件年龄:${this.user.info.age}`).fontSize(22);

      Button("父组件修改年龄(直接修改)")
        .onClick(() => {
         // 如果 UserInfoCard 注释掉你再试试能不能修改?
          this.user.info.age += 2;
        });

      Button("父组件修改年龄(替换引用)")
        .onClick(() => {
          this.user.info = new UserInfo(this.user.info.age + 2);
        });
      
      UserInfoCard({ user: this.user });
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#f5f5f5')
    .padding(20);
  }
}
运行效果

属性深层监听

3.3 二维数组深度监听示例

// pages/ObjectLinkTwoDArrayPage.ets
import { UserInfoCard } from '../components/UserInfoCard';
import { UserInfo, UserModel } from '../model/UserModel';

@Observed
class ObservedArray<T> extends Array<T> {
  constructor(...args: T[]) {
    super(...args);
  }
}

@Component
struct ArrayChild {
  @ObjectLink arr: ObservedArray<UserModel>;

  build() {
    Column({ space: 8 }) {
      Text("子组件 - UserModel类型数组展示")
        .fontSize(18)
        .fontWeight(FontWeight.Medium);

      Row({ space: 8 }) {
        ForEach(this.arr, (user: UserModel) => {
          Column(){
            Column({ space: 4 }) {
              Text(`姓名:${user.name}`).fontSize(14);
              Text(`年龄:${user.info.age}`).fontSize(12).fontColor('#666');
            }
            .padding(10)
            .backgroundColor('#f0f0f0')
            .borderRadius(6)
            .onClick(() => { 
              // 如果 UserInfoCard 注释掉你再试试能不能修改?
              this.arr[0].name  = "修改后的用户名"
            })
          
            UserInfoCard({user:user})
          }
        })
      }
      Button("子组件修改用户信息")
        .margin(10)
        .onClick(() => {
          if (this.arr.length >0) {
            this.arr[0] = new UserModel("修改后的用户名",new UserInfo(30));
          }
        });
    }
    .backgroundColor('#e0e0e0')
    .borderRadius(8);
  }
}

@Entry
@Component
struct ObjectLinkTwoDArrayPage {
  @State twoDUserArr: ObservedArray<UserModel>[] = [
    new ObservedArray(
      new UserModel("张三", new UserInfo(20)),
      new UserModel("李四", new UserInfo(22))),
    new ObservedArray(
      new UserModel("王五", new UserInfo(25)),
      new UserModel("赵六", new UserInfo(28)))
  ];

  build() {
    Column({ space: 20 }) {
      Text("父组件 - UserModel类型二维数组深度监听")
        .fontSize(28)
        .fontWeight(FontWeight.Bold);

      Column({ space: 8 }) {
        ForEach(this.twoDUserArr, (rowArr: ObservedArray<UserModel>) => {
          ArrayChild({ arr: rowArr });
        })
      }

      Button("父组件新增一行用户数据")
        .onClick(() => {
          const newRow = new ObservedArray(
            new UserModel(`新用户${Math.floor(Math.random() * 100)}`, new UserInfo(18)),
            new UserModel(`新用户${Math.floor(Math.random() * 100)}`, new UserInfo(19)));
          this.twoDUserArr.push(newRow);
          this.twoDUserArr = [...this.twoDUserArr];
        });
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#f5f5f5')
    .padding(20);
  }
}

3.4 常见错误与约束

  1. 嵌套 class 漏写 @Observed → 嵌套属性修改不刷新。
  2. @ObjectLink 直接写在 @Entry 组件中 → 编译报错。
  3. 未为嵌套对象提供独立UI层,直接修改属性不刷新。
    解决方案:① 分层组件+@ObjectLink;② 替换引用。
  4. 对 @ObjectLink 变量整体重新赋值 → 运行时异常。

四、@Track 装饰器(class 属性级精准刷新)

4.1 概述

@Track 是 class 对象的属性装饰器,核心解决 @State 对「多属性class对象」的全量冗余刷新问题。
当 class 对象作为状态变量时:

  • @Track 装饰的属性变化 → 只触发该属性关联的 UI 更新
  • 如果 class 使用了 @Track,则未被 @Track 装饰的属性不能在 UI 中使用,否则运行时报错。

4.2 为什么需要 @Track

状态管理 V1 中 @State 只能观察整个class对象的第一层属性变化,无法做到「单个属性级」的精准观察——哪怕class只有一层、多个独立属性,修改其中一个属性也会导致所有关联该class的UI组件全量刷新。

我们通过日志打印直观验证这个问题:

import { hilog } from '@kit.PerformanceAnalysisKit';
const DOMAIN_NUMBER: number = 0XFF00;
const TAG: string = '[Sample_StateTrack]';

// 多属性class
class User {
  public name: string = '张三';
  public age: number = 20;
  public gender: string = '男';
}

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

  // 借助getFontSize的日志打印,可以分辨哪个组件触发了渲染
  getFontSize(id: number): number {
    hilog.info(DOMAIN_NUMBER, TAG, `Component ${id} render`);
    return 30;
  }

  build() {
    Column() {
      // 组件1:仅关联name属性
      Text(`姓名:${this.user.name}`)
        .fontSize(this.getFontSize(1))
      // 组件2:仅关联age属性
      Text(`年龄:${this.user.age}`)
        .fontSize(this.getFontSize(2))
      // 组件3:仅关联gender属性
      Text(`性别:${this.user.gender}`)
        .fontSize(this.getFontSize(3))

      // 仅修改name,但组件1/2/3全量刷新(日志打印3条render)
      Button('修改姓名').onClick(() => {
        this.user.name = '李四';
      })

      // 仅修改age,同样触发组件1/2/3全量刷新
      Button('修改年龄').onClick(() => {
        this.user.age++;
      })
    }
    .height('100%')
    .width('100%')
  }
}
运行结果(冗余刷新问题):
  • 首次渲染日志:Component 1 render + Component 2 render + Component 3 render
  • 点击「修改姓名」:3个组件全部刷新(age/gender组件无意义刷新)
  • 点击「修改年龄」:3个组件全部刷新(name/gender组件无意义刷新)

冗余刷新

造成冗余刷新的根本原因:@State 以「整个class对象」为观察单位,而非「单个属性」。为了实现类对象属性的精准观察、消除冗余渲染,引入 @Track 装饰器。

4.3 核心规则

  1. @Track 只能装饰 class 内部非静态属性,不能装饰组件变量。
  2. 一旦 class 使用 @Track,未被 @Track 装饰的属性禁止出现在 UI 中,否则运行时报错。
  3. 未被 @Track 装饰的属性可以在非 UI 逻辑中使用(回调、生命周期等)。
  4. @Track 无深度观测能力(仅监听自身属性,不监听嵌套属性)。
  5. 不建议在联合类型、继承中混用带/不带 @Track 的 class。

4.4 示例代码(TrackPage.ets)

// pages/TrackPage.ets
import { TrackDemo } from '../model/UserModel';
import { hilog } from '@kit.PerformanceAnalysisKit';
const DOMAIN_NUMBER: number = 0XFF00;
const TAG: string = '[TrackDemo]';

@Component
struct TrackChild {
  @Link trackData: TrackDemo;

  // 日志打印验证组件刷新范围
  getFontSize(id: number): number {
    hilog.info(DOMAIN_NUMBER, TAG, `子组件${id}刷新`);
    return 18;
  }

  build() {
    Column({ space: 10 }) {
      // 仅关联name,修改name时仅该组件刷新
      Text(`子组件 - 姓名:${this.trackData.name}`)
        .fontSize(this.getFontSize(1))
        .backgroundColor('#e0f7fa');
      
      // 仅关联gender,修改gender时仅该组件刷新
      Text(`子组件 - 性别:${this.trackData.gender}`)
        .fontSize(this.getFontSize(2))
        .backgroundColor('#f3e5f5');

      // 错误示例:age未@Track,不能在UI中使用(运行时报错)
      // Text(`子组件 - 年龄:${this.trackData.age}`)

      Button("子组件修改姓名")
        .onClick(() => {
          this.trackData.name = "修改后的姓名✨";
        });

      Button("子组件修改性别")
        .onClick(() => {
          this.trackData.gender = this.trackData.gender === "男" ? "女" : "男";
        });

      // 合法示例:未@Track的age可在非UI逻辑(点击回调)中使用
      Button("子组件修改年龄(非UI)")
        .onClick(() => {
          this.trackData.age += 1;
          hilog.info(DOMAIN_NUMBER, TAG, `年龄修改(非UI):${this.trackData.age}`);
        });
    }
    .padding(20)
    .backgroundColor('#f5f5f5')
    .borderRadius(12);
  }
}

@Entry
@Component
struct TrackPage {
  @State trackData: TrackDemo = new TrackDemo("鸿蒙开发者", 20, "男");

  // 日志打印验证父组件刷新范围
  getFontSize(id: number): number {
    hilog.info(DOMAIN_NUMBER, TAG, `父组件${id}刷新`);
    return 22;
  }

  build() {
    Column({ space: 20 }) {
      // 组件1 
      Text("@Track 装饰器 - 属性级精准刷新示例")
        .fontSize(28)
        .fontWeight(FontWeight.Bold);

      // 组件2 
      Text(`父组件 - 姓名:${this.trackData.name}`)
        .fontSize(this.getFontSize(1));
      // 组件3 
      Text(`父组件 - 性别:${this.trackData.gender}`)
        .fontSize(this.getFontSize(2));

      Button("父组件修改姓名")
        .onClick(() => {
          this.trackData.name = "父组件修改姓名📌";
        });

      Button("父组件修改性别")
        .onClick(() => {
          this.trackData.gender = this.trackData.gender === "男" ? "女" : "男";
        });

      TrackChild({ trackData: $trackData });
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#f5f5f5')
    .padding(20);
  }
}
运行效果:
  • 点击「父组件修改姓名」:仅打印 父组件1刷新 + 子组件1刷新(性别组件无刷新)
  • 点击「父组件修改性别」:仅打印 父组件2刷新 + 子组件2刷新(姓名组件无刷新)
  • 实现「改哪个属性,只刷新关联该属性的组件」,彻底消除冗余渲染。

Track解决冗余刷新问题

4.5 常见错误

  1. UI 中使用未 @Track 的属性 → 运行时报错
  2. 用 @Track 装饰组件变量 → 编译报错
  3. 期望 @Track 支持深度监听 → 不支持(仅监听当前属性)
  4. API 19+ ComponentV2 中 @Track 不刷新 → 官方设计(仅不报错,无刷新效果)

五、@Provide + @Consume 跨层级状态共享

5.1 核心定义与作用

用于实现祖先组件与任意后代组件之间的双向状态同步,不需要逐层传递参数

@Provide
  • 装饰目标:祖先组件中的变量
  • 语法:@Provide("key") 变量名: 类型 = 初始值
@Consume
  • 装饰目标:后代组件中的变量
  • 语法:@Consume("key") 变量名: 类型

5.2 完整示例

// pages/ProvideConsumePage.ets
import { UserInfo, UserModel } from '../model/UserModel';
import { UserAgeItem } from '../components/UserAgeItem';

@Component
struct GreatGrandChild {
  @Consume("currentUser") currentUser: UserModel;

  build() {
    Column({ space: 8 }) {
      Text(`曾孙组件 - 用户名:${this.currentUser.name}`).fontSize(16);
      Text(`曾孙组件 - 年龄:${this.currentUser.info.age}`).fontSize(14).fontColor('#666');
      
      Button("曾孙组件修改年龄")
        .onClick(() => {
          this.currentUser.info.age += 1;
        });
      
      UserAgeItem({ userInfo: this.currentUser.info });
    }
  }
}

@Component
struct GrandChild {
  @Consume("count") count: number;
  @Consume("currentUser") currentUser: UserModel;

  build() {
    Column({ space: 10 }) {
      Text(`孙子组件计数:${this.count}`).fontSize(20);
      Text(`孙子组件 - 用户名:${this.currentUser.name}`).fontSize(16);
      
      Button("孙子组件 +1")
        .onClick(() => {
          this.count++;
        });
      GreatGrandChild();
    }
  }
}

@Component
struct Child {
  build() {
    Column() {
      GrandChild();
    }
  }
}

@Entry
@Component
struct ProvideConsumePage {
  @Provide("count") count: number = 0;
  @Provide("currentUser") currentUser: UserModel = new UserModel("跨层级用户", new UserInfo(25));

  build() {
    Column({ space: 20 }) {
      Text("根组件 - 跨层级共享页面").fontSize(28).fontWeight(FontWeight.Bold);

      Text(`根组件计数:${this.count}`).fontSize(24);
      Text(`根组件 - 用户名:${this.currentUser.name}`).fontSize(18);
      Text(`根组件 - 年龄:${this.currentUser.info.age}`).fontSize(16).fontColor('#666');

      Button("根组件 +1")
        .onClick(() => {
          this.count++;
        });

      Button("根组件修改用户名")
        .onClick(() => {
          this.currentUser.name = "根组件修改后的用户名";
        });
      Child();
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#f5f5f5');
  }
}

六、状态管理 V1 全装饰器对比总结

装饰器/组合 核心能力 适用场景 关键特点
@State 组件内部私有状态,浅观察,全量刷新 单个组件内部状态管理 第一层属性变更触发整个组件刷新
@Prop 父子单向同步,深拷贝 子组件仅展示父组件数据,无需修改 子改父不变
@Link 父子双向同步,引用传递 父子组件需双向修改同一数据 需用 $ 符号绑定
@Observed + @ObjectLink 嵌套class深度监听,双向同步 多层嵌套class对象状态管理 一层属性对应一层UI组件
@Provide + @Consume 跨层级双向同步,无需逐层传参 祖孙/跨多层级组件共享数据 基于key绑定,双向同步
@Track class属性级精准刷新,消除冗余渲染 多属性class对象性能优化 仅刷新关联属性的UI,无深度监听

七、核心总结

  1. @Observed+@ObjectLink:一层属性对应一层UI,才能直接修改嵌套属性并触发刷新。
  2. @Track:针对class多属性,实现「改哪个属性只刷新哪个属性关联的UI」,彻底消除冗余渲染;UI中用到的属性必须全部标记@Track,无深度监听能力。
  3. @Provide+@Consume:跨层级共享状态,嵌套对象刷新规则与@Observed+@ObjectLink一致。
  4. 整套状态管理 V1 体系:内部状态 → 父子同步 → 深度监听 → 跨层级 → 性能优化(@Track)。

九、下节预告

下一节我们将学习ArkTS 自定义组件与页面的生命周期理解生命周期的作用与意义,区分自定义组件生命周期与页面生命周期;
掌握 aboutToCreate、aboutToAppear、onAppear、onDisappear、aboutToDisappear、onDestroy 等组件生命周期函数的执行时机与用途;

  • 掌握页面独有生命周期:onPageShow、onPageHide、onBackPress;
  • 学会在正确的生命周期中初始化数据、开启定时器、请求接口、清理资源,避免内存泄漏;
Logo

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

更多推荐