【学习目标】

  1. 理解 ArkTS 状态管理的核心概念与运行原理。
  2. 掌握 @State、@Prop、@Link、@Watch 四个基础装饰器的使用场景与核心差异。
  3. 能独立编写父子组件同步、状态监听等实战代码,并理解执行逻辑。
  4. 识别并规避状态管理中的常见错误与使用限制。

一、状态管理核心概念

在 ArkTS 声明式 UI 开发中,状态管理是实现“数据驱动视图”的核心机制,用于将数据与 UI 组件绑定,让数据变化自动触发 UI 刷新,无需手动操作 DOM/组件。

状态管理区分V1和V2版本,当前我们先讲解V1版本,旧项目用的比较多新项目直接使用V2版本。

  • 状态变量:被 @State/@Prop 等装饰器修饰的变量,是驱动 UI 变化的核心;变量值变化时,框架会自动刷新关联的 UI 组件。
  • 常规变量:未被任何装饰器修饰的普通变量,仅用于临时存储/计算,其值变化不会触发任何 UI 刷新。
  • 数据流向:状态变量在组件间的传递规则,分为「单向同步」(仅父传子)和「双向同步」(父子互传)。
  • 依赖收集:组件首次渲染(执行 build 函数)时,框架会自动记录“哪些 UI 组件依赖了哪些状态变量”,建立一一对应的绑定关系。
  • 触发更新:当状态变量值变化时,框架仅刷新“依赖该变量的 UI 组件”(标记为“脏节点”),而非全局刷新,保证性能最优。

核心思想:数据驱动视图,而非视图驱动数据

二、状态管理基本原理

状态更新的完整流程

  1. 依赖收集阶段
    组件首次渲染时,框架遍历 build 函数中的 UI 组件,记录每个状态变量对应的 UI 组件(例如:Text(${count}) 依赖 @State count),并将依赖关系存储在框架内部。

  2. 触发更新阶段

    • 当状态变量值发生变化(如点击按钮修改 count);
    • 框架根据依赖关系,标记“使用该变量的 UI 组件”为“脏节点”;
    • 下一帧(VSync 信号)到来时,仅刷新这些“脏节点”,其他 UI 组件保持不变;
    • 刷新完成后,重新收集依赖(处理动态绑定场景)。

关键优势:最小化更新,避免无意义的全局刷新,提升应用性能。

三、工程结构与准备

3.1 环境要求

  • 系统版本:HarmonyOS API 12 及以上
  • 开发工具:DevEco Studio 5.0 及以上

3.2 工程目录结构

StateManagementDemo/
├── AppScope/
│   └── app.json5                # 应用全局配置
├── entry/
│   ├── src/
│   │   ├── main/
│   │   │   ├── ets/
│   │   │   │   ├── entryability/
│   │   │   │   │   └── EntryAbility.ets    # 应用入口
│   │   │   │   ├── model/                   # 数据模型层(新增)
│   │   │   │   │   └── UserModel.ets        # 定义Class对象
│   │   │   │   ├── pages/
│   │   │   │   │   ├── Index.ets           # 导航页(跳转各示例)
│   │   │   │   │   ├── StatePage.ets       # @State 示例
│   │   │   │   │   ├── PropPage.ets        # @Prop 示例
│   │   │   │   │   ├── LinkPage.ets        # @Link 示例
│   │   │   │   │   └── WatchPage.ets       # @Watch 示例
│   │   │   └── module.json5                # 模块配置(页面路由、权限等)

3.3 数据模型定义(UserModel.ets)

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

export class UserModel {
  name: string;
  info: UserInfo;
  constructor(name: string, info: UserInfo) {
    this.name = name;
    this.info = info;
  }
}

3.4 导航页(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' },
  ];

  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');
  }
}

运行效果

@State & @Prop 演示 @Link & @Watch 演示
状态管理@State@Prop 状态管理@Link@Watch

四、核心装饰器详解

4.1 @State 组件内部状态

核心知识点
  • 作用:定义组件私有的内部状态,仅当前组件可访问和修改,是最基础的状态装饰器。
  • 初始化要求:必须显式指定类型 + 本地初始化(如 @State count: number = 0),否则编译报错。
  • 支持类型:
    • 基础类型:string、number、boolean、enum;
    • 复杂类型:Class 实例、数组、Date、Map/Set;
    • 不支持:any 类型、function 类型。
  • 观察特性:浅观察——仅监听变量“第一层属性”的变化,嵌套 Class 实例的属性(如 user.info.age)修改不会触发 UI 刷新。
  • 生命周期:与组件绑定,组件销毁时状态变量也会被销毁。
示例代码(StatePage.ets)
import { UserInfo, UserModel } from '../model/UserModel';

@Entry
@Component
struct StatePage {
  // 必须初始化 + 指定类型(基础类型)
  @State count: number = 0;
  // 浅观察示例
  @State user: UserModel = new UserModel("散修", new UserInfo(4));

  build() {
    Column({ space: 20 }) {
      // 1. 基础类型状态演示
      Text(`当前计数:${this.count}`)
        .fontSize(24);
      Button("点击 +1")
        .onClick(() => {
          this.count++; // 修改状态,触发UI刷新
        });

      // 2. 浅观察特性演示(Class实例)
      Text(`姓名:${this.user.name},年龄:${this.user.info.age}`)
        .fontSize(18);

      Text(`年龄:${this.user.info.age}`)
        .fontSize(18);

      Button("修改姓名(触发刷新)")
        .onClick(() => {
          // 第一层属性修改,触发UI刷新
          this.user.name = "散修鸿蒙应用开发";
        });

      Button("修改年龄(不刷新)")
        .onClick(() => {
          // 嵌套Class属性修改,不触发UI刷新
          this.user.info.age += 1;
          console.log("年龄已改:", this.user.info.age); // 仅控制台输出,UI不变
        });

      Button("重新赋值User(触发刷新)")
        .onClick(() => {
          // 重新赋值第一层(整个Class实例),触发UI刷新
          this.user = new UserModel("散修鸿蒙应用开发", new UserInfo(5));
        });
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#f5f5f5');
  }
}
运行效果
  • 点击“点击 +1”:计数文本实时更新;
  • 点击“修改姓名”:姓名文本刷新,年龄不变;
  • 点击“修改年龄”:控制台打印年龄变化,但 UI 无更新(浅观察特性);
  • 点击“重新赋值User”:姓名和年龄文本均刷新(重新赋值第一层)。
常见错误避坑

错误:未初始化 → @State count: number;(编译报错);
正确:必须赋值 → @State count: number = 0

错误:修改嵌套 Class 属性期望刷新 → this.user.info.age = 5
替代方案:重新赋值第一层 → this.user = new UserModel("ArkTS", new UserInfo(5))

4.2 @Prop 父子单向同步

核心知识点
  • 作用:实现父组件 → 子组件的单向数据同步,子组件可本地修改,但不会回传给父组件。
  • 初始化要求:
    • 子组件可本地初始化(如 @Prop count: number = 0);
    • 若未本地初始化,父组件必须通过构造参数传值(否则编译报错);
    • 父传值会覆盖子组件的本地初始化值。
  • 支持类型:与 @State 一致(基础类型 + Class 实例、数组、Date、Map/Set)。
  • 传递方式:深拷贝——父组件传递的是数据副本,而非引用;
    • 基础类型:直接拷贝值;
    • Class 实例:拷贝实例属性,除 Map/Set/Date/Array 外,复杂 Class 会丢失原型方法。
  • 核心规则:
    • 父组件修改数据 → 子组件 @Prop 同步更新(覆盖子本地修改);
    • 子组件修改 @Prop → 仅子组件内部生效,父组件数据不变。
  • 性能建议:嵌套层数不超过 5 层,否则会增加 GC 压力,推荐用 @ObjectLink 替代。
示例代码(PropPage.ets)
import { UserInfo, UserModel } from '../model/UserModel';

// 子组件:接收父组件的@Prop
@Component
struct PropChild {
  // 可本地初始化,父传值会覆盖(基础类型)
  @Prop count: number = 0;
  // Class类型@Prop
  @Prop user: UserModel = new UserModel("默认名称", new UserInfo(0));

  build() {
    Column({ space: 10 }) {
      Text(`子组件 @Prop(计数):${this.count}`)
        .fontSize(20);
      Text(`子组件 @Prop(姓名):${this.user.name}`)
        .fontSize(20);
      Button("子组件 +1(不回传)")
        .onClick(() => {
          this.count++; // 仅子组件内部修改,父组件不变
        });
      Button("修改子组件姓名(不回传)")
        .onClick(() => {
          this.user.name = "子组件修改的名称"; // 仅子组件生效
        });
    }
    .padding(20)
    .backgroundColor('#e0e0e0')
    .borderRadius(10);
  }
}

// 父组件:提供@State数据源
@Entry
@Component
struct PropPage {
  @State parentCount: number = 0;
  @State parentUser: UserModel = new UserModel("父组件初始名称", new UserInfo(10));

  build() {
    Column({ space: 20 }) {
      Text(`父组件 @State(计数):${this.parentCount}`)
        .fontSize(24);
      Text(`父组件 @State(姓名):${this.parentUser.name}`)
        .fontSize(24);
      Button("父组件 +1(同步子)")
        .onClick(() => {
          this.parentCount++; // 父修改,子@Prop同步更新
        });
      Button("修改父组件姓名(同步子)")
        .onClick(() => {
          this.parentUser.name = "父组件修改的名称"; // 父修改,子@Prop同步更新
        });

      // 父传子:直接传递@State变量(基础类型+Class类型)
      PropChild({ count: this.parentCount, user: this.parentUser });
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#f5f5f5');
  }
}
运行效果
  • 点击“父组件 +1”:父计数文本和子计数文本同时更新;
  • 点击“修改父组件姓名”:父姓名文本和子姓名文本同时更新;
  • 点击“子组件 +1”:仅子计数文本更新,父计数文本不变;
  • 点击“修改子组件姓名”:仅子姓名文本更新,父姓名文本不变;
  • 再次点击“修改父组件姓名”:子姓名文本被父组件的值覆盖,回到同步状态。
核心区别(@State vs @Prop)
特性 @State @Prop
数据流向 组件内部 父 → 子
传递方式 - 深拷贝
初始化 必须本地初始化 可选本地初始化
数据修改 自身修改触发刷新 子修改不回传父
复杂对象支持 Class 实例(推荐) Class 实例(深拷贝)

4.3 @Link 父子双向同步

核心知识点
  • 作用:实现父组件 ↔ 子组件的双向数据同步,父子共享同一份数据,一方修改另一方实时同步。
  • 初始化要求:禁止本地初始化(如 @Link count: number;),必须由父组件通过 $变量名 传值,否则编译报错。
  • 支持类型:与 @State 一致(基础类型 + Class 实例、数组、Date、Map/Set)。
  • 传递方式:引用传递——子组件直接使用父组件的数据源引用,而非拷贝(性能优于 @Prop);
    • Class 实例:父子共享同一个实例,修改实例属性会同步(但浅观察特性仍生效)。
  • 核心规则:
    • 父修改数据 → 子组件 @Link 同步更新;
    • 子修改 @Link → 父组件数据源同步更新;
  • 使用限制:不能在 @Entry 组件中直接使用(@Entry 无父组件)。
示例代码(LinkPage.ets)
import { UserInfo, UserModel } from '../model/UserModel';

// 子组件:接收父组件的@Link
@Component
struct LinkChild {
  // 禁止本地初始化!必须父传值(基础类型)
  @Link linkCount: number;
  // Class类型@Link
  @Link linkUser: UserModel;

  build() {
    Column({ space: 10 }) {
      Text(`子组件 @Link(计数):${this.linkCount}`)
        .fontSize(20);
      Text(`子组件 @Link(姓名):${this.linkUser.name}`)
        .fontSize(20);
      Button("子组件 +1(同步父)")
        .onClick(() => {
          this.linkCount++; // 子修改,父同步更新
        });
      Button("修改子组件姓名(同步父)")
        .onClick(() => {
          this.linkUser.name = "子组件修改的名称"; // 子修改,父同步更新
        });
    }
    .padding(20)
    .backgroundColor('#e0e0e0')
    .borderRadius(10);
  }
}

// 父组件:提供@State数据源
@Entry
@Component
struct LinkPage {
  @State count: number = 0;
  @State user: UserModel = new UserModel("父组件初始名称", new UserInfo(10));

  build() {
    Column({ space: 20 }) {
      Text(`父组件 @State(计数):${this.count}`)
        .fontSize(24);
      Text(`父组件 @State(姓名):${this.user.name}`)
        .fontSize(24);
      Button("父组件 +1(同步子)")
        .onClick(() => {
          this.count++; // 父修改,子同步更新
        });
      Button("修改父组件姓名(同步子)")
        .onClick(() => {
          this.user.name = "父组件修改的名称"; // 父修改,子同步更新
        });

      // 父传子:必须加 $ 符号(传递引用,基础类型+Class类型)
      LinkChild({ linkCount: $count, linkUser: $user });
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#f5f5f5');
  }
}
运行效果
  • 点击“父组件 +1”:父计数文本和子计数文本同时更新;
  • 点击“修改父组件姓名”:父姓名文本和子姓名文本同时更新;
  • 点击“子组件 +1”:子计数文本和父计数文本同时更新(双向同步);
  • 点击“修改子组件姓名”:子姓名文本和父姓名文本同时更新(双向同步)。
常见错误避坑

错误:父传值不加 $ → LinkChild({ linkCount: this.count })(编译报错);
正确:必须传引用 → LinkChild({ linkCount: $count })

错误:子组件本地初始化 → @Link linkCount: number = 0(编译报错);
正确:仅声明类型 → @Link linkCount: number;

错误:@Link 传递字面量对象 → @Link user: { name: string } = {...}
正确:传递 Class 实例 → @Link user: UserModel;

4.4 @Watch 状态变化监听

核心知识点
  • 作用:监听状态变量(@State/@Prop/@Link 等)的变化,变量值修改时触发指定回调函数。
  • 语法规则:
    • 书写位置:紧跟状态装饰器之后 → @State @Watch("回调函数名") count: number = 0
    • 回调参数:默认接收 changedPropertyName(字符串类型),表示“变化的状态变量名”;
    • 触发时机:仅当变量值“真正变化”时触发(严格相等 === 判断),初始化时不触发。
  • 核心限制:
    • 禁止在回调函数中修改“当前监听的变量”(会导致无限循环触发回调);
    • 回调函数需为同步函数,不支持 async/await
    • 仅监听“可观察的状态变化”(如 @State 的浅观察范围,嵌套 Class 属性修改不触发)。
  • 适用场景:状态变化后执行额外逻辑(如表单校验、日志记录、数据计算)。
示例代码(WatchPage.ets)
import { UserInfo, UserModel } from '../model/UserModel';

@Entry
@Component
struct WatchPage {
  // 单个变量监听(基础类型)
  @State @Watch("onCountChange") count: number = 0;
  // 多个变量共用一个监听回调(Class类型+基础类型)
  @State @Watch("onCommonChange") user: UserModel = new UserModel("HarmonyOS", new UserInfo(4));
  @State @Watch("onCommonChange") age: number = 4;
  // 监听日志展示
  @State log: string = "未触发任何监听";

  // 单个变量的监听回调
  onCountChange(changedPropertyName: string) {
    this.log = `[${new Date().toLocaleTimeString()}] ${changedPropertyName} 变化,新值:${this.count}`;
    // 禁止:this.count++ → 会无限循环触发回调
  }

  // 多个变量共用的监听回调
  onCommonChange(changedPropertyName: string) {
    let newValue = changedPropertyName === "user" ? this.user.name : this.age.toString();
    this.log = `[${new Date().toLocaleTimeString()}] ${changedPropertyName} 变化,新值:${newValue}`;
  }

  build() {
    Column({ space: 20 }) {
      // 1. 单个变量监听演示(基础类型)
      Text(`当前计数:${this.count}`)
        .fontSize(24);
      Button("修改计数(触发监听)")
        .onClick(() => {
          this.count += 2; // 触发onCountChange回调
        });

      // 2. 多变量监听演示(Class类型+基础类型)
      Text(`当前姓名:${this.user.name},当前年龄:${this.age}`)
        .fontSize(18);
      Row({ space: 10 }) {
        Button("修改姓名(触发监听)")
          .onClick(() => {
            this.user = new UserModel("ArkTS", new UserInfo(5)); // 重新赋值Class实例,触发监听
          });
        Button("修改年龄(触发监听)")
          .onClick(() => {
            this.age++; // 触发onCommonChange回调
          });
        Button("修改嵌套年龄(不触发)")
          .onClick(() => {
            this.user.info.age++; // 嵌套属性修改,不触发监听
          });
      }

      // 3. 监听日志展示
      Text("监听日志:")
        .fontSize(18)
        .fontWeight(FontWeight.Bold);
      Text(this.log)
        .fontSize(16)
        .fontColor(Color.Blue);
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#f5f5f5');
  }
}
运行效果
  • 初始化时:日志显示“未触发任何监听”(符合“初始化不触发”规则);
  • 点击“修改计数”:日志显示 count 变化,新值:2
  • 点击“修改姓名”:日志显示 user 变化,新值:ArkTS
  • 点击“修改年龄”:日志显示 age 变化,新值:5
  • 点击“修改嵌套年龄”:日志无变化(嵌套属性修改不触发监听)。
常见错误避坑

错误:回调中修改自身监听变量 →

onCountChange() {
  this.count++; // 无限循环触发回调,应用卡死
}

正确:仅修改其他变量或执行无状态逻辑 →

onCountChange() {
  this.log = "计数变化了"; // 安全操作
}

错误:监听嵌套 Class 属性修改 → this.user.info.age++ 期望触发 @Watch;
正确:重新赋值第一层 Class 实例 → this.user = new UserModel(...)

五、核心装饰器对比总结

装饰器 核心作用 数据流向 初始化要求 传递方式 复杂对象支持 关键特性
@State 组件内部私有状态 组件内 必须本地初始化 - Class 实例(推荐) 浅观察,仅第一层属性可监听
@Prop 父子单向同步 父 → 子 可选本地初始化 深拷贝 Class 实例(拷贝) 子修改不回传,父修改覆盖子
@Link 父子双向同步 父 ↔ 子 禁止本地初始化,需传$ 引用传递 Class 实例(共享) 父子共享数据,双向更新
@Watch 监听状态变化 - 无(仅装饰状态变量) - Class 实例(浅监听) 初始化不触发,禁止自修改

记忆口诀

  1. @State:自己用,必须初始化,浅观察,复杂对象用 Class;
  2. @Prop:父传子,单向同步,深拷贝,子改不回传;
  3. @Link:父子双向同步,引用传递,必须带 $,共享 Class 实例;
  4. @Watch:监听变化,初始化不触发,仅监第一层,不修改自身防死循环。

六、常见问题与避坑指南

1. 状态变化但 UI 不刷新?

  • 原因1:修改了 @State 嵌套 Class 属性(浅观察特性);
    解决:重新赋值第一层 Class 实例 → this.user = new UserModel("ArkTS", new UserInfo(5))
  • 原因2:修改了常规变量(未加装饰器);
    解决:给变量添加 @State 等装饰器;
  • 原因3:在子线程修改状态变量;
    解决:切换到 UI 主线程修改;

2. @Prop / @Link 编译报错?

  • @Prop 报错:父组件未传值且子未本地初始化;
    解决:要么父传值,要么子本地初始化;
  • @Link 报错:父传值未加 $ 或子本地初始化;
    解决:父传值加 $,子仅声明类型;
  • @Link 报错:传递字面量对象;
    解决:传递 Class 实例 → LinkChild({ linkUser: $user })

3. @Watch 不触发?

  • 原因1:变量值未真正变化(如 count = 0 再次赋值 0);
    解决:确保值发生实际变化;
  • 原因2:监听了嵌套 Class 属性修改(浅观察限制);
    解决:重新赋值第一层 Class 实例;
  • 原因3:回调函数名书写错误(如 @Watch("onCountChange") 写成 @Watch("onCount"));
    解决:检查回调函数名拼写一致。

七、核心总结

  1. ArkTS 状态管理V1的核心是通过装饰器实现数据驱动视图,核心装饰器包括@State(组件内私有状态)、@Prop(父子单向同步)、@Link(父子双向同步)、@Watch(状态监听);
  2. @State具备浅观察特性,仅监听第一层属性变化,嵌套Class属性修改需重新赋值整个实例才能触发UI刷新;
  3. @Prop采用深拷贝传递数据(子改不回传父),@Link采用引用传递(父子双向同步),使用@Link必须通过$符号传递且禁止本地初始化。

八、仓库代码

  • 工程名称:StateManagementDemo
  • 仓库地址:https://gitee.com/HarmonyOS-UI-Basics/harmony-os-ui-basics.git

九、下节预告

第二十节:ArkTS 状态管理V1进阶

  • @Observed/@ObjectLink:解决 Class 实例嵌套属性的深度监听问题(突破 @State 浅观察限制);
  • @Provide/@Consume:实现跨层级组件的状态传递(无需逐层传参);
Logo

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

更多推荐