【HarmonyOS应用开发入门】第五期:状态管理V2入门 - 1

一、状态管理V2概述

1、V1到V2的演进:为什么需要状态管理V2?

比喻:小区快递管理

假设你的 App 是一个小区 ,数据是住户的快递 ,UI 是快递柜

状态管理 V1(老系统)

工作方式: 给每个快递柜配一个专属的快递员(代理观察者),快递员只看自己柜子里的快递单,不看住户本人。

问题:

  1. 住户搬家不知道(数据不独立)

    • 你把快递从"1号楼301"改到"2号楼502",但只告诉了快递员A。
    • 快递员B还在傻等"1号楼301"的快递,导致其他快递柜显示错误。
  2. 看不清快递内容(深度观察不了)

    • 快递员只看外包装标签,如果快递里是一个包裹套包裹(对象嵌套对象),里面的小包裹换了,快递员发现不了。
  3. 整柜换新(冗余更新)

    • 住户只是换了一个小物品,但快递员嫌麻烦,把整个快递柜清空重放,导致刷新很慢。
  4. 职责不清(组件化难)

    • 快递员、快递柜、住户关系混乱,谁该管什么不清楚,很难模块化管理。

状态管理 V2(新系统)

工作方式: 不再给柜子配快递员,而是给每个快递本身装上 GPS 追踪芯片。快递去哪,系统自动通知所有相关快递柜。

改进:

  1. 快递独立(数据独立)

    • 快递(数据)本身带有追踪能力,无论放到哪个快递柜,都能自动更新显示。
    • ✅ 数据真正和 UI 解耦。
  2. 透视眼(深度观察)

    • 哪怕快递里套了 10 层小包裹,最里面换了个小零件,系统也能精准感知,并通知到相关快递柜。
    • ✅ 性能还更好。
  3. 精准更换(最小化更新)

    • 住户只换了一个小物品,系统就只更新那个小格子,其他格子不动。
    • ✅ 更新更快、更省资源。
  4. 职责分明(组件化易)

    • 系统明确区分:这是输入的快递@Prop),这是输出的快递@Link)。
    • 模块之间像搭积木一样清晰,方便复用。

核心区别对比表

特性 V1(老系统) V2(新系统)
数据与UI关系 绑定特定UI,数据不独立 数据独立,可被多个UI观察
观察深度 只能看第一层(浅观察) 能看任意深度(深观察)
更新粒度 经常整个对象/数组重刷(粗) 只更新变了的属性/元素(细)
组件化 装饰器限制多,输入输出模糊 装饰器清晰易用,输入输出明确
性能 嵌套数据观察性能差 深度观察但性能优化好

一句话总结

状态管理 V2 就像给数据本身装上了"智能追踪器" ,让数据变化能被更深度、更精准、更独立 地感知和响应,从而让应用开发更高效、性能更好、组件更清晰

二、装饰器详解

1、@ObservedV2装饰器和@Trace装饰器:类属性变化观测

延续以往的风格,小鱼用一个简单的"智能家庭监控系统"来比喻,让你轻松理解这两个装饰器的作用和规则。


比喻:智能家庭监控系统

想象你要监控家里各个位置的情况:

  • 你的家 = 一个被 @ObservedV2 装饰的类
  • 家里的物品(门窗、电视、冰箱) = 类中的属性
  • 给物品装的传感器 = @Trace 装饰器
  • 手机上的监控App = UI 界面

规则解释

i. 必须配套使用(缺一不可)

"只装监控摄像头(@ObservedV2),不给物品贴传感器(@Trace),摄像头不知道看哪里。只给物品贴传感器(@Trace),不装总监控系统(@ObservedV2),信号发不出去。"

typescript 复制代码
@ObservedV2
class House {          // 装了整个家的监控系统
  @Trace door: number = 1;    // 给门装了传感器 ✅
  window: number = 2;         // 窗户没装传感器 ❌
}

ii. 精准通知(谁变通知谁)

"只有装了传感器的物品变化,才会触发手机App通知。没装传感器的物品变化,系统不知道,App也不会更新。"

typescript 复制代码
@ObservedV2
class House {
  @Trace door: number = 1;    // 门开关 → App收到通知
  window: number = 2;         // 窗户开关 → App不知道 ❌
}

iii. 嵌套房间也要装监控

"如果你家还有个小书房(嵌套类),书房里的保险箱要监控,必须:①给书房装监控(@ObservedV2),②给保险箱装传感器(@Trace)"

typescript 复制代码
@ObservedV2
class StudyRoom {      // 书房也要装监控系统
  @Trace safe: number = 1;    // 保险箱装传感器 ✅
}

@ObservedV2
class House {
  @Trace mainRoom: StudyRoom = new StudyRoom(); // 书房作为属性
}

iv. 父子房间同样规则

"爸爸的房间(父类)和孩子的房间(子类)规则一样:想监控哪个物品,就在那个房间装监控系统并给物品装传感器。"

typescript 复制代码
@ObservedV2
class ParentRoom {
  @Trace parentBed: number = 1;  // 父母的床 ✅
}

@ObservedV2
class ChildRoom extends ParentRoom {
  @Trace toyBox: number = 2;     // 孩子的玩具箱 ✅
}

v. 重要限制
  • 不能序列化

    "这个智能监控系统很特殊,不能拍成照片(JSON.stringify)发给别人看。"

  • 必须新建实例

    "必须是新建的房子(new House()),二手房(已存在的对象)装不了这个系统。"


实际代码示例

typescript 复制代码
@ObservedV2
class User {
  @Trace name: string = ''; // UI会更新 ✅
  @Trace age: number = 18; // UI会更新 ✅
  hobby: string = ''; // UI不会更新 ❌

  updateName() {
    this.name = '夏小鱼'; // UI自动刷新
  }
}

@Entry
@ComponentV2
export struct Index {
  user = new User();

  aboutToAppear(): void {
    this.user.name = 'Xiaxiaoyu';
  }

  build() {
    Column({space:10}){
      Text(`name: ${this.user.name} age: ${this.user.age} hobby: ${this.user.hobby}`);
      Button('改名字').onClick(()=>{
        this.user.updateName();
      })
      Button('改年龄').onClick(()=>{
        this.user.age = 20;
      })

      Button('改喜好').onClick(()=>{
        this.user.hobby = 'coding';
      })
    }
    .alignSelf(ItemAlign.Center)
    .alignItems(HorizontalAlign.Center)
    .width('100%').height('100%')
  }
}

一句话总结

@ObservedV2 + @Trace 就像给数据装"智能传感器系统":

  • @ObservedV2 = 安装整个监控系统
  • @Trace = 给具体物品装传感器
  • 规则:两样都要装,且只监控装了传感器的物品
  • 好处 :UI 更新精准高效,只变哪里就更新哪里

2、@ComponentV2装饰器:自定义组件

好的,我用一个更简单的比喻来解释 @ComponentV2 和它的规则。


比喻:新版"智能家电" vs 旧版"传统家电"

想象你要买家电:

  • 旧版家电(@Component 组件) = 传统家电,功能齐全但有些笨重
  • 新版家电(@ComponentV2 组件) = 智能新款,更轻便但暂时功能少一些

主要规则解释

i. 必须用新版配件

"买了新版智能家电,就只能用配套的新版智能遥控器和传感器(@Local@Param@Once 等),不能用旧版的配件。"

typescript 复制代码
@ComponentV2
struct SmartFridge {          // 新版智能冰箱
  @Local foodCount: number = 0;   // 只能用新版温度传感器 ✅
  // @State foodCount: number = 0;  // 不能用旧版传感器 ❌
}

ii. 暂时少了些功能

"新版智能家电刚上市,暂时没有'自动除霜'(LocalStorage)等功能,等以后更新。"

typescript 复制代码
@ComponentV2
struct SmartFridge {
  // 暂时不支持 LocalStorage、AppStorage 等功能
  // 以后版本会慢慢加上
}

iii. 不能新旧混用

"不能把一个家电同时标注为'传统版'又'智能版',只能二选一。"

typescript 复制代码
// ❌ 错误:不能同时用两个装饰器
@Component
@ComponentV2  // 冲突!
struct MyComponent {
}

// ✅ 正确:只能选一个
@ComponentV2
struct MySmartComponent {
}

iv. 新增"休眠省电"模式

"新版智能家电多了个'休眠模式'(freezeWhenInactive),不使用时自动休眠省电。"

typescript 复制代码
@ComponentV2(true)  // 启用休眠模式
struct SmartFridge {
  // 当这个组件不在屏幕上时,会自动冻结状态来节省资源
}

v. 基本结构不变

"虽然升级了,但基本外观和使用方式(struct + build)和老款差不多。"

typescript 复制代码
@ComponentV2
struct MyComponent {          // 1. 结构声明
  @Local count: number = 0;   // 2. 状态变量
  
  build() {                   // 3. UI构建(和以前一样)
    Text(`计数: ${this.count}`)
  }
}

实际代码对比

传统版(@Component

typescript 复制代码
@Component
struct OldComponent {
  @State count: number = 0;        // 用@State
  @StorageLink('key') data: number; // 支持LocalStorage
  
  build() {
    Text(`旧版: ${this.count}`)
  }
}

智能新版(@ComponentV2

typescript 复制代码
@ComponentV2
struct NewComponent {
  @Local count: number = 0;        // 改用@Local
  // 暂不支持LocalStorage等功能
  
  build() {                         // build写法一样
    Text(`新版: ${this.count}`)
  }
}

一句话总结

@ComponentV2 就是组件的"升级版":

  • 新配件 :必须用全新的状态管理装饰器(@Local@Param 等)
  • 轻量起步:暂时少了些高级功能,但以后会补上
  • 二选一:不能新旧装饰器混用
  • 新增功能:多了"休眠模式"节省资源
  • 习惯不变:基本写法(struct + build)和以前一样

3、@Local装饰器:组件内部状态

小鱼用一个"个人日记本 "的比喻来解释 @Local 装饰器。


比喻:个人日记本

想象 @Local 装饰的变量就像你的个人日记本

  • 日记本 = 被 @Local 装饰的变量
  • = 当前组件
  • 写日记 = 修改变量的值
  • 自动同步到云备份 = UI 自动刷新

规则解释

i. 自己买自己写(内部初始化)

"这本日记必须你自己买、自己写第一页,别人不能帮你写开头。"

typescript 复制代码
@ComponentV2
struct MyDiary {
  @Local diaryContent: string = "今天天气不错"; // ✅ 自己初始化
  // @Local diaryContent: string;              // ❌ 不初始化会报错
}

ii. 一写就备份(自动刷新)

"只要你写了新内容(修改变量),系统立刻自动备份(刷新UI)。"

typescript 复制代码
@Local diaryContent: string = "初始内容";

// 当你修改时
this.diaryContent = "今天学会了ArkTS"; // UI自动更新

iii. 能记各种内容(支持多种类型)

"日记本能记录文字、数字、清单、画图等各种内容。"

typescript 复制代码
@Local count: number = 0;           // 数字 ✅
@Local name: string = "小明";       // 文字 ✅  
@Local scores: number[] = [90, 95]; // 清单 ✅
@Local config: {theme: string} = {theme: "dark"}; // 对象 ✅

iv. 观察规则不同(按类型区分)

根据内容类型,观察的精细度不同:

① 简单内容(数字、文字)

typescript 复制代码
@Local score: number = 90;
this.score = 95;  // ✅ 整页重写,系统知道

② 画册(对象)

typescript 复制代码
@ObservedV2
class  Album {
  @Trace photos: string[]=[];
}

 @Local album: Album = {photos: []};
 this.album = {photos: ["a.jpg"]};//✅ 换整本画册,系统知道
this.album.photos?.push('b.jpg'); // ❌ 对象内部修改,UI不刷新(除非@Trace)

③ 清单(数组)

typescript 复制代码
@Local todoList: string[] = ["吃饭", "睡觉"];
this.todoList = ["学习"];        // ✅ 换整个清单,系统知道
this.todoList.push("运动");      // ✅ 加一项,系统也知道(数组特殊)

④ 特殊本子(Set/Map/Date)

typescript 复制代码
@Local dates: Set<string> = new Set();
this.dates.add("2024-01-01");    // ✅ 用自带方法修改,系统知道

v. 可以留空或多种类型(null/联合类型)

"可以暂时不写(null),或者记不同类型的内容。"

typescript 复制代码
@Local note: string | null = null;  // ✅ 可以留空
@Local data: string | number = 0;   // ✅ 可以是文字或数字
this.data = "变成文字";            // ✅ 类型变化也能感知

实际使用示例

typescript 复制代码
@ObservedV2
class  Album {
  photos: string[]=[];
}

@Entry
@ComponentV2
struct PersonalNote {
  // 1. 必须自己初始化
  @Local text: string = "默认内容";
  @Local count: number = 0;
  @Local list: string[] = ["第一项"];
  @Local config: Config = {color: "blue"};
  @Local album: Album = new Album();

  build() {
    Column() {
      // 2. 修改这些变量都会自动刷新UI
      Text(this.text)
        .onClick(() => {
          this.text = "点击后新内容";  // ✅ UI刷新

        })

      Text(`列表长度: ${this.list.length}`)
        .onClick(() => {
          this.list.push("新项目");    // ✅ 数组push,UI刷新
          // this.config.color = "red"; // ❌ 对象内部修改,UI不刷新
        })


      Text(`相册: ${this.album.photos?.length}`)
        .onClick(() => {
          this.album.photos?.push('b.jpg'); // ❌ 对象内部修改,UI不刷新
          // this.album = {photos: ["a.jpg"]};//✅ 换整本画册,系统知道
        })
    }
  }
}

一句话总结

@Local 就是组件的"个人笔记本":

  • 自己保管:必须组件内部初始化
  • 自动同步:一修改就刷新UI
  • 内容多样:支持各种数据类型
  • 观察规则
    • 简单类型:改了就刷新
    • 对象:只认整本替换
    • 数组/Set/Map:方法修改也能感知
  • 灵活:支持空值和联合类型

4、@Param:组件外部输入

小鱼用一个"共享文件夹 "的比喻来解释 @Param 装饰器。


比喻:共享文件夹

想象父子组件之间通过 @Param 传递数据就像使用共享文件夹

  • 父组件 = 办公室的共享服务器
  • 子组件 = 你的个人电脑
  • @Param 变量 = 从服务器下载到电脑的"共享文件夹"
  • 同步规则 = 文件同步机制

规则解释

i. 可以放空文件夹(支持本地初始化)

"你可以先创建一个空文件夹在电脑上,但内容要从服务器同步过来。"

typescript 复制代码
@ComponentV2
struct MyComputer {
  @Param sharedFile: string = "空文件"; // ✅ 可以先放个空文件
  // 但不能直接修改:this.sharedFile = "新内容" ❌
}

ii. 服务器更新,你自动同步(单向同步)

"当服务器上的文件更新时,你电脑上的文件夹会自动同步新内容。"

typescript 复制代码
// 父组件(服务器)
@Local serverData: string = "原始数据";

// 传递给子组件
ChildComponent({ sharedFile: this.serverData })

// 当父组件修改:
this.serverData = "更新数据"; // ✅ 子组件自动收到更新

iii. 可以同步各种文件(接受任意数据源)

"服务器可以给你共享文档、表格、图片、压缩包等各种文件。"

typescript 复制代码
// 父组件可以传各种数据:
ChildComponent({
  sharedData: "字符串",        // ✅ 普通文字
  sharedNumber: this.count,   // ✅ 状态变量
  sharedConst: MAX_SIZE,      // ✅ 常量
  sharedResult: getData(),    // ✅ 函数返回值
})

iv. 你修改了,别人也能看到(引用类型特殊)

"如果是共享的'Excel表格文件',你在电脑上修改内容,服务器上也能看到更新。"

typescript 复制代码
// 假设共享的是一个对象(Excel文件)
class ExcelFile {
  data: string[] = ["A", "B"];
}

// 父组件
@Local serverFile: ExcelFile = new ExcelFile();

// 子组件
@Param myFile: ExcelFile;

// 子组件内可以修改属性:
this.myFile.data.push("C"); // ✅ 修改内容,父组件也能看到!
// this.myFile = new ExcelFile(); // ❌ 但不能替换整个文件

v. 观察规则(和@Local类似)

根据文件类型,观察的精细度不同:

① 简单文件(txt文档)

typescript 复制代码
@Param textFile: string;
// 只能整个文件替换,不能改其中一行

② 对象文件(Excel/Word)

typescript 复制代码
@Param excelFile: Excel;
// 可以改里面的单元格(属性)
// 但不能换整个文件

③ 列表文件(文件夹)

typescript 复制代码
@Param fileList: string[];
// 可以增删文件(数组方法)
// 也可以替换整个文件夹

实际父子组件示例

typescript 复制代码
// ========== 子组件:接收共享文件 ==========

@ObservedV2
class Config {
  @Trace color: ResourceColor = Color.White;
}


@ComponentV2
struct ChildComponent {
  @Param message: string = "夏小鱼"; // 从父组件接收
  @Param config: Config = { color: Color.Blue }; // 接收对象

  build() {
    Column() {
      Text(this.message)  // 显示父组件传来的消息
        .fontColor(this.config.color) // 使用对象属性

      Button("修改配置")
        .onClick(() => {
          // this.message = "新消息"; // ❌ 不允许直接修改
          this.config.color = Color.Green; // ✅ 可以修改对象内部属性,但UI不更
        })
    }
  }
}

// ========== 父组件:提供共享文件 ==========
@Entry
@ComponentV2
struct ParentComponent {
  @Local parentMessage: string = "来自父组件的问候";
  @Local parentConfig: Config = {color: "blue"};

  build() {
    Column() {
      // 把数据共享给子组件
      ChildComponent({
        message: this.parentMessage,  // 传递字符串
        config: this.parentConfig     // 传递对象(引用)
      })

      Button("更新数据")
        .onClick(() => {
          this.parentMessage = "父组件更新了";  // ✅ 子组件会自动更新
          // 由于传递的是引用,子组件修改config.color也会反映到这里
        })
    }
  }
}

核心要点总结

  1. 单向同步(主要):父 → 子(父更新,子自动更新)
  2. 双向修改(特殊情况) :如果是对象/数组,子组件可以修改内部属性,父组件也能看到
  3. 不能直接赋值 :子组件不能 this.param = 新值
  4. 支持所有类型:字符串、数字、对象、数组等都能传
  5. 自动刷新UI:参数变化时,使用它的UI会自动更新

一句话总结

@Param 就是组件的"共享文件夹":

  • 来源:从父组件获得数据
  • 同步:父更新 → 子自动更新
  • 特殊权利:如果是对象/数组,可以修改内部内容并同步回父组件
  • 限制:不能直接替换整个"文件夹"

5、@Once:初始化同步一次

这次用一个"一次性快递包裹 "的比喻来解释 @Once 装饰器。


比喻:一次性快递包裹

想象 @Once 就像你给朋友寄一个一次性密封的快递包裹

  • 父组件 = 你(寄件人)
  • 子组件 = 朋友(收件人)
  • @Param = 普通快递(可以持续更新)
  • @Once + @Param = 一次性密封快递(只收一次,后续更新不接收)

规则解释

i. 必须配合使用(密封袋+包裹)

"不能只给'一次性密封袋'(@Once),必须里面有实际的'包裹内容'(@Param)。"

typescript 复制代码
@ComponentV2
struct FriendHouse {
  // ✅ 正确:密封袋 + 包裹
  @Once @Param gift: string = "默认礼物";
  
  // ❌ 错误:只有密封袋
  // @Once empty: string;
  
  // ❌ 错误:密封袋装其他东西
  // @Once @Local localGift: string;
}

ii. 只收第一次(拦截后续更新)

"朋友只收你第一次寄的包裹,后续你再寄更新的包裹,朋友家不收。"

typescript 复制代码
// ========== 父组件(你)==========
@Local myGift: string = "最新款手机";

// 第一次寄出
ChildComponent({ gift: this.myGift }) // 朋友收到:"最新款手机"

// 你升级了礼物
this.myGift = "旗舰版手机"; // 但朋友不接收这个更新

iii. 朋友可以自己改装(允许本地修改)

"朋友收到后,可以自己拆开改装这个礼物,但这不影响你那边。"

typescript 复制代码
@ComponentV2
struct FriendHouse {
  @Once @Param gift: string;
  
  build() {
    Column() {
      Text(this.gift)
      Button("改装礼物")
        .onClick(() => {
          // ✅ 朋友可以自己修改收到的礼物
          this.gift = "改装后的" + this.gift;
          // 但这个修改不会传回给你(寄件人)
        })
    }
  }
}

iv. 顺序无所谓(包装方式)

"先装包裹再套密封袋,还是先套密封袋再装包裹,效果都一样。"

typescript 复制代码
// 两种写法效果相同:
@Once @Param data: string;   // 写法1
@Param @Once data: string;   // 写法2 ✅ 顺序无所谓

实际使用场景示例

场景:配置初始化后不再改变
typescript 复制代码
// ========== 子组件:显示产品信息 ==========
@ComponentV2
struct ProductCard {
  @Once @Param productName: string = "未命名"; // 只初始化一次
  @Once @Param initialPrice: number = 0; // 初始价格,后续不更新

  build() {
    Column({ space: 10 }) {
      Text(`产品:${this.productName}`)
      Text(`首发价:${this.initialPrice}元`)
      Text(`当前状态:${this.getStatus()}`)
    }.padding(10).backgroundColor(Color.Gray).width('90%').margin(10).borderRadius(10)
  }

  getStatus() {
    // 基于初始价格计算状态,不受后续价格波动影响
    return this.initialPrice > 1000 ? "高端产品" : "普通产品";
  }
}

// ========== 父组件:管理产品 ==========
@Entry
@ComponentV2
struct ProductManager {
  @Local name: string = "智能手机X";
  @Local price: number = 2999;
  @Local currentPrice: number = 2599; // 促销降价

  build() {
    Column({ space: 10 }) {
      // 传递初始数据(一次性)
      ProductCard({
        productName: this.name, // 只传这一次
        initialPrice: this.price     // 首发价,后续降价不影响
      })

      Text(`当前售价:${this.currentPrice}元`)

      Button("产品升级")
        .onClick(() => {
          this.name = "智能手机X Pro"; // 改了,但子组件不会更新
          this.price = 3999; // 改了,但子组件不会更新
          this.currentPrice = 3599; // 只是当前售价变了
        })
    }
  }
}

为什么需要 @Once?

想象以下实际场景:

  1. 版本信息:App版本号只需要初始化时传一次,后续更新不需要同步
  2. 用户首次选择:用户第一次选择的配置,后续父组件改了也不影响子组件
  3. 快照数据:需要保留某个时刻的数据快照
  4. 性能优化:明确知道某些数据只需要传一次,避免不必要的同步开销

核心要点总结

  1. 黄金搭档 :必须和 @Param 一起使用
  2. 一次性接收:只收父组件的第一次传值
  3. 本地自由:子组件可以随意修改这个值
  4. 单向隔离:父组件后续更新影响不到子组件
  5. 顺序灵活@Once@Param 谁先谁后都一样

一句话总结

@Once 就是给 @Param 加上"一次性封印":

  • 第一次接收:正常从父组件拿初始值
  • 然后封印:父组件后续更新全部屏蔽
  • 自己处置:子组件可以随意修改这个值
  • 互不影响:从此父子组件在这个数据上各自独立

6、@Event装饰器:规范组件输出

用一个"父子间对讲机 "的比喻来解释 @Event 装饰器。


比喻:父子间对讲机

想象父子组件之间通过 @Event 通信就像使用对讲机

  • 父组件 = 指挥中心
  • 子组件 = 前线小分队
  • @Param = 指挥中心发给小分队的指令(单向:父 → 子)
  • @Event = 小分队报告情况的对讲机按钮(单向:子 → 父)
  • @Local = 指挥中心内部的状态

核心问题与解决方案

问题:小分队不能直接改指令

"指挥中心发来指令(@Param),小分队只能接收不能直接修改。"

解决方案:用对讲机请求修改

"小分队按下对讲机(调用@Event):'报告!请求更新指令!' → 指挥中心收到 → 修改内部状态(@Local) → 新指令自动同步给小分队(通过@Param)"

typescript 复制代码
// ========== 小分队(子组件)==========
@ComponentV2
struct ChildTeam {
  @Param order: string = "待命";     // 收到的指令(不能直接改)
  @Event requestChange: (newOrder: string) => void; // 对讲机按钮
  
  build() {
    Column() {
      Text(`当前指令:${this.order}`)
      Button("请求进攻")
        .onClick(() => {
          // ❌ 不能直接改:this.order = "进攻";
          // ✅ 正确做法:用对讲机请求
          this.requestChange("进攻"); // "报告!请求改为进攻!"
        })
    }
  }
}

// ========== 指挥中心(父组件)==========
@ComponentV2
struct CommandCenter {
  @Local currentOrder: string = "待命"; // 真正的指令源
  
  build() {
    Column() {
      // 1. 传递指令
      // 2. 传递对讲机频道(收到报告后做什么)
      ChildTeam({
        order: this.currentOrder, // 传递指令
        requestChange: (newOrder: string) => { // 定义对讲机功能
          this.currentOrder = newOrder; // 收到报告,修改指令
          // 修改后,会自动同步给子组件的@Param变量
        }
      })
    }
  }
}

规则解释

i. 必须是对讲机按钮(只能是回调函数)

"@Event 只能装饰'按钮',不能装饰其他东西。"

typescript 复制代码
@ComponentV2
struct Team {
  @Event report: () => void;           // ✅ 正确:一个按钮
  @Event sendData: (data: string) => boolean; // ✅ 正确:带参数的按钮
  
  @Event wrong: string = "错误";       // ❌ 错误:不是按钮,@Event无效
}

ii. 可以自带默认按钮(本地默认值)

"如果指挥中心没给对讲机,小分队可以自己带个备用喇叭。"

typescript 复制代码
@ComponentV2
struct Team {
  // 如果父组件没提供,就用这个默认函数
  @Event report: () => void = () => {
    console.log("使用默认报告方式");
  };
}

iii. 完全没按钮就自动静音(自动生成空函数)

"如果既没收到对讲机,也没带喇叭,就自动装个'静音按钮'(按了没反应)。"

typescript 复制代码
@ComponentV2
struct Team {
  @Event report: () => void; // 父组件没传,自己也没默认值
  // 系统会自动生成:report = () => {}
}

iv. 清晰分工:输入 vs 输出

@Param = 输入(你听指挥)
@Event = 输出(你提建议)

typescript 复制代码
@ComponentV2
struct SmartComponent {
  @Param inputData: string;    // ← 从父组件输入(只能收)
  @Event outputAction: () => void; // → 向父组件输出(主动发)
}

完整工作流程示例

typescript 复制代码
// ========== 子组件:计数器显示和控制按钮 ==========
@ComponentV2
struct CounterDisplay {
  @Param count: number = 0;                // 显示的数字(从父来)
  @Event onIncrement: () => void;          // 增加按钮的回调
  @Event onReset: (newValue: number) => void; // 重置按钮的回调
  
  build() {
    Column() {
      Text(`计数:${this.count}`)
        .fontSize(30)
      
      Button("+1")
        .onClick(() => {
          this.onIncrement(); // 通知父组件:"请增加计数"
        })
      
      Button("重置为10")
        .onClick(() => {
          this.onReset(10); // 通知父组件:"请重置为10"
        })
    }
  }
}

// ========== 父组件:真正的数据管理者 ==========
@ComponentV2
struct ParentPage {
  @Local realCount: number = 0; // 真正的计数器
  
  build() {
    Column() {
      // 传递数据和事件处理器
      CounterDisplay({
        count: this.realCount, // 传递当前值
        onIncrement: () => {   // 定义"增加"时做什么
          this.realCount++;    // 修改真实数据
        },
        onReset: (newValue: number) => { // 定义"重置"时做什么
          this.realCount = newValue;
        }
      })
      
      Text(`父组件实际值:${this.realCount}`)
        .fontColor(Color.Red)
    }
  }
}

实际应用场景

  1. 表单组件:子组件收集输入,通过 @Event 提交给父组件
  2. 弹窗组件:通过 @Event 通知父组件用户点击了确定/取消
  3. 列表项:每个列表项通过 @Event 通知父组件被点击了
  4. 自定义按钮:按钮通过 @Event 通知点击事件

核心要点总结

  1. 解决限制:@Param 不能直接改 → 用 @Event 请求父组件改
  2. 单向通信:子 → 父(与 @Param 的父 → 子相反)
  3. 必须是函数:只能装饰回调函数类型
  4. 默认值支持:可以设置本地默认函数
  5. 清晰接口:明确组件的输入(@Param)和输出(@Event)

一句话总结

@Event 就是子组件的"对讲机按钮":

  • 用途 :子组件通过它请求父组件修改数据
  • 规则 :只能装饰函数,用来定义组件的输出接口
  • 流程:子按钮 → 父接收 → 父修改 @Local → 自动同步回子 @Param
  • 意义 :实现子组件间接修改接收到的数据

7、@Provider装饰器和@Consumer装饰器:跨组件层级双向同步

小鱼用一个"家族广播电台 "的比喻来解释 @Provider@Consumer


比喻:家族广播电台

想象一个大家族(组件树)里有一个家族广播系统

  • @Provider = 族长家的广播电台(发射信号)
  • @Consumer = 每个家庭成员的对讲机(接收信号)
  • key = 广播频道频率
  • 组件层级 = 家族辈分关系

核心机制图示

复制代码
族长家(@Provider 数据源)
    │ 广播信号
    ├── 大伯家
    │    ├── 大堂哥(@Consumer 收到信号)
    │    └── 小堂妹(@Consumer 收到信号)
    │
    └── 二叔家
         ├── 二堂姐(@Consumer 收到信号)
         └── 堂弟的朋友(隔得太远,收不到信号,用默认值)

规则解释

i. 族长广播,全族收听(跨层级传播)

"族长(@Provider)在广播电台讲话,所有晚辈(子组件、孙子组件...)只要调对频道(key)都能听到,不用一层层传话。"

typescript 复制代码
// ========== 族长家(顶层组件)==========
@Entry
@ComponentV2
struct ChiefFamily {
  @Provider() familyNews: string = "家族通知:下周聚会";

  build() {
    Column({ space: 10 }) {
      Text("族长广播中...")
      // 所有子组件都能直接收到 familyNews
      UncleFamily() // 大伯家
      EldestCousin() // 大堂哥家
    }.backgroundColor(Color.Green)
    .padding(10)
    .borderRadius(10)
  }
}

ii. 调对频道才能听(相同key)

"你的对讲机(@Consumer)必须调到族长电台的频道(相同key)才能听到。"

typescript 复制代码
// ========== 大堂哥(深层子组件)==========
@ComponentV2
struct EldestCousin {
  @Consumer() familyNews: string = "默认消息"; // ✅ 频道一致,收到广播

  build() {
    Text(`大堂哥收到家族消息:${this.familyNews}`).backgroundColor(Color.Gray).padding(10).borderRadius(10)
  }
}

// ========== 大伯家(中间层,也装了电台)==========
@ComponentV2
struct UncleFamily {
  @Provider() familyNews: string = "大伯通知:今晚来我家吃饭"; // 自己的电台

  build() {
    Column({ space: 10 }) {
      Text(`大伯收到家族消息:${this.familyNews}`)
      Text("大伯广播中...")
      // 这里的子组件收到的是"今晚来我家吃饭",不是族长的"下周聚会"
      YoungerCousin()
    }.backgroundColor(Color.Blue).borderRadius(10).padding(10)
  }
}

iii. 最近族长优先(就近原则)

"如果你家有分电台(更近的@Provider),就听你家的,不听大族长的。"

typescript 复制代码
// ========== 大伯家(中间层,也装了电台)==========
@ComponentV2
struct UncleFamily {
  @Provider() familyNews: string = "大伯通知:今晚来我家吃饭"; // 自己的电台

  build() {
    Column({ space: 10 }) {
      Text(`大伯收到家族消息:${this.familyNews}`)
      Text("大伯广播中...")
      // 这里的子组件收到的是"今晚来我家吃饭",不是族长的"下周聚会"
      YoungerCousin()
    }.backgroundColor(Color.Blue).borderRadius(10).padding(10)
  }
}


// ========== 小堂妹(底层成员)==========
@ComponentV2
struct YoungerCousin {
  @Consumer() familyNews: string = "默认";

  build() {
    Column({ space: 10 }) {
      Text(`小堂妹收到家族消息:${this.familyNews}`)
      Button("小堂妹建议")
        .onClick(() => {
          this.familyNews = "小堂妹建议:改周末聚会"; // 她说话,全族都更新!
        })
    }.backgroundColor(Color.Gray).padding(10).borderRadius(10)
  }
}

iv. 双向同步(都能修改)

"不仅族长能广播,任何人通过对讲机说话,全族都能听到更新。"

typescript 复制代码
// ========== 族长家 ==========
@ComponentV2
struct ChiefFamily {
  @Provider familyNews: string = "初始通知";
  
  build() {
    Column() {
      Button("族长更新")
        .onClick(() => {
          this.familyNews = "族长新通知"; // 族长说话,全族更新
        })
      
      UncleFamily()
    }
  }
}

// ========== 小堂妹(底层成员)==========
@ComponentV2
struct YoungerCousin {
  @Consumer familyNews: string = "默认";
  
  build() {
    Button("小堂妹建议")
      .onClick(() => {
        this.familyNews = "小堂妹建议:改周末聚会"; // 她说话,全族都更新!
      })
  }
}

iv. 收不到信号就用默认(查找不到时)

"如果离得太远或调错频道,对讲机就用自带的默认消息。"

typescript 复制代码
@ComponentV2
struct DistantRelative {
  @Consumer() familyNews: string = "未收到家族消息"; // 默认值
  
  build() {
    // 如果找不到任何@Provider,就显示"未收到家族消息"
    Text(this.familyNews)
  }
}

v. 祖孙三代收到消息
javascript 复制代码
@Entry
@ComponentV2
struct ChiefFamily {
  @Provider() familyNews: string = "家族通知:下周聚会";

  build() {
    Column({ space: 10 }) {
      Text("族长广播中..." + this.familyNews)

      // 所有子组件都能直接收到 familyNews
      Button("族长更新")
        .onClick(() => {
          this.familyNews = "族长新通知"; // 族长说话,全族更新
        })

      UncleFamily()
    }.backgroundColor(Color.Green)
    .padding(10)
    .borderRadius(10)
  }
}
//
// ========== 大伯家(中间层层)==========
@ComponentV2
struct UncleFamily {
  @Consumer() familyNews: string = "大伯通知:今晚来我家吃饭"; // 自己的电台

  build() {
    Column({ space: 10 }) {
      Text(`大伯收到家族消息:${this.familyNews}`)
      // 这里的子组件收到的是"今晚来我家吃饭",不是族长的"下周聚会"
      YoungerCousin()
    }.backgroundColor(Color.Blue).borderRadius(10).padding(10)
  }
}
//
// ========== 小堂妹(底层成员)==========
@ComponentV2
struct YoungerCousin {
  @Consumer() familyNews: string = "家族通知:下周聚会";

  build() {
    Column({ space: 10 }) {
      Text(`小堂妹收到家族消息:${this.familyNews}`)
      Button("小堂妹建议")
        .onClick(() => {
          this.familyNews = "小堂妹建议:改周末聚会"; // 她说话,全族都更新!
        })
      DistantRelative();
    }.backgroundColor(Color.Gray).padding(10).borderRadius(10)

  }
}



@ComponentV2
struct DistantRelative {
  @Consumer() familyNews: string = "未收到家族消息"; // 默认值

  build() {
    // 如果找不到任何@Provider,就显示"未收到家族消息"
    Text(this.familyNews).backgroundColor(Color.Yellow).width('90%').padding(10).borderRadius(10)
  }
}

实际代码示例

typescript 复制代码
// ========== 主题管理(全局)==========
@Entry
@ComponentV2
struct ThemeManager {
  @Provider() themeColor: string = "blue"; // 提供主题色

  build() {
    Column({ space: 10 }) {
      Text("主题管理器")
        .fontColor(this.themeColor)

      Button("切换红色主题")
        .onClick(() => {
          this.themeColor = "red"; // 修改后,所有消费者自动更新
        })

      // 深层嵌套的组件
      PageContent()
    }.backgroundColor(Color.Blue)
    .padding(10)
    .width('100%').alignItems(HorizontalAlign.Center)
  }
}

// ========== 页面内容(中间层)==========
@ComponentV2
struct PageContent {
  build() {
    Column({ space: 10 }) {
      Header() // 头部组件
      MainContent() // 主要内容
      Footer() // 底部组件
    }
    .width('90%')
    .backgroundColor(Color.Green)
    .borderRadius(10)
    .padding(10)
  }
}

// ========== 头部组件(任意层级)==========
@ComponentV2
struct Header {
  @Consumer() themeColor: string = "gray"; // 消费主题色

  build() {
    Text("应用标题")
      .fontColor(this.themeColor) // 自动使用当前主题色
      .backgroundColor(this.themeColor === "dark" ? "#333" : "#FFF")
      .width('90%')
      .borderRadius(10)
      .padding(10)
  }
}

@ComponentV2
struct MainContent {
  @Consumer() themeColor: string = "gray"; // 消费主题色

  build() {
    Text("主要内容")
      .fontColor(this.themeColor) // 自动使用当前主题色
      .backgroundColor(this.themeColor === "dark" ? "#333" : "#FFF")
      .width('90%')
      .borderRadius(10)
      .padding(10)
  }
}


// ========== 底部组件(另一个任意层级)==========
@ComponentV2
struct Footer {
  @Consumer() themeColor: string = "gray";

  build() {
    Button("底部:切换深色主题")
      .onClick(() => {
        this.themeColor = "dark"; // 这里修改,所有地方都变!
      })
  }
}

使用注意事项(重要!)

⚠️ 强依赖层级(家族辈分)

"你的位置(组件层级)决定了你能收到哪个电台的信号。"

⚠️ 减少使用(保持独立)

"就像不要用胶水把全家人都粘在一起,尽量保持组件独立,只在必要时用广播系统。"

适用场景
  1. 主题切换:所有组件都要同步主题色
  2. 用户登录状态:全局共享登录信息
  3. 多语言:全局语言设置
  4. 全局配置:如字体大小、深色模式
不适用场景
  1. 父子组件简单传值:用 @Param 更合适
  2. 独立组件:不需要广播的组件

与@Param的对比

特性 @Param(传话) @Provider/Consumer(广播)
传播方式 一层层传话 直接广播,跨层级
修改权限 子不能直接改父 双方都能改,双向同步
耦合度 低(明确接口) 高(隐式依赖)
适用场景 父子直接通信 全局/跨层级状态共享

一句话总结

@Provider@Consumer 就是组件的"家族广播系统":

  • 族长广播:@Provider 发布数据
  • 全族收听:任何层级的 @Consumer 都能接收(相同key)
  • 双向通话:谁修改了,所有人都同步更新
  • 就近原则:先听最近的 @Provider
  • 慎用:会提高耦合度,只在需要全局状态时使用

8、@Monitor装饰器:状态变量修改监听

小鱼用一个"智能安保摄像头 "的比喻来解释 @Monitor 装饰器。


比喻:智能安保摄像头

想象 @Monitor 就像你家安装的智能安保摄像头系统

  • 被监控的物品 = 状态变量(@Local@Param 等装饰的)
  • 摄像头 = @Monitor 装饰的回调函数
  • 监控规则 = 摄像头的触发条件
  • 物品变化 = 有人动了物品

核心机制图示

复制代码
客厅
├── 保险柜(@Local money = 1000)
│       └── 📹 摄像头(@Monitor 监控 money)
├── 电视(@Param tv = {brand: "索尼"})
│       └── 📹 摄像头(@Monitor 监控 tv)
└── 书架(@ObservedV2 Book + @Trace pages)
        └── 📹 摄像头(@Monitor 监控 pages)

规则解释

i. 只监控贵重物品(必须是状态变量)

"摄像头只监控贴了'贵重物品标签'(状态装饰器)的东西,普通物品不监控。"

typescript 复制代码
@Entry
@ComponentV2
struct Home {
  @Local money: number = 1000; // ✅ 贵重物品,能被监控
  @Param tv: TV = new TV(); // ✅ 能被监控
  normalItem: string = "普通物品"; // ❌ 没贴标签,不监控

  @Monitor('money')
  // ✅ 可以监控money
  onMoneyChanged(monitor: IMonitor) {
    monitor.dirty.forEach((path: string) => {
      console.log(`金额变化:${monitor.value(path)?.before} → ${monitor.value(path)?.now}`);
      if (typeof monitor.value(path)?.now === 'number') {
        this.money = monitor.value(path)?.now as number;
      }
    })
  }

  build() {
    Column({ space: 10 }) {
      Button(`存钱 ${this.money}`).onClick(() => {
        this.money = this.money + 200;
      })
    }
  }
}

@ObservedV2
class TV {
}

ii. 监控智能保险箱(配合@ObservedV2 + @Trace):监听深层属性变化

"要监控保险箱里的具体物品,需要:①保险箱本身是智能的(@ObservedV2),②物品贴了标签(@Trace)"

示例一单属性:

typescript 复制代码
@ObservedV2
class SafeBox { // 智能保险箱
  @Trace gold: number = 100; // 贴标签的金条 ✅ 能被监控
  silver: number = 200; // 没贴标签的银条 ❌ 不被监控
}

@Entry
@ComponentV2
struct Bank {
  @Local mySafe: SafeBox = new SafeBox();
  @Local tips:string = '';

  @Monitor('mySafe.gold')
  // ✅ 监控金条变化
  onGoldChanged() {
    console.log("金条数量变化了!");
    this.tips = '金条数量变化了!' 
  }

  build() {

    Button(`金条+200: ${this.tips}`).onClick(() => {
      this.mySafe.gold += 200;
    })
  }
}

示例二批量监控:

typescript 复制代码
@ObservedV2
class SafeBox { // 智能保险箱
  @Trace gold: number = 100; // 贴标签的金条 ✅ 能被监控
  @Trace silver: number = 200; // 贴标签的金条 ✅ 能被监控
}

@ObservedV2
class CashBox {
  @Trace cash: number = 888;
}


@Entry
@ComponentV2
struct Bank {
  @Local mySafe: SafeBox = new SafeBox();
  @Local myCashBox: CashBox = new CashBox();
  @Local tips: string = '';

  // ✅ 监控变化
  @Monitor('mySafe.gold','mySafe.silver','myCashBox.cash')
  onGoldChanged(monitor: IMonitor) {
    monitor.dirty.forEach((path: string) => {
      console.log(`金额变化:${monitor.value(path)?.before} → ${monitor.value(path)?.now}`);
      this.tips += `${monitor.value(path)?.before} → ${monitor.value(path)?.now}\n`;
    });
    this.tips +='\n';
  }

  build() {
    Column({ space: 10 }) {
      Text(`${this.tips}`)
      Button(`金条+20, 银条-10`).onClick(() => {
        this.mySafe.gold += 200;
        this.mySafe.silver -= 10;
        this.myCashBox.cash += 111;
      })
    }.padding(20)
  }
}

iii. 透视眼监控(深度监听)

"摄像头能看透嵌套的盒子,监控最里面的物品,但需要每层都贴标签。"

typescript 复制代码
@ObservedV2
class InnerBox {
  @Trace diamond: string = "大钻石"; // 最里面的钻石
}

@ObservedV2
class OuterBox {
  @Trace inner: InnerBox = new InnerBox(); // 里面的盒子
}


@Entry
@ComponentV2
struct JewelryStore {
  @Local box: OuterBox = new OuterBox();
  @Local tips: string = '';

  @Monitor('box.inner.diamond')
  // ✅ 能监控三层嵌套的钻石
  onDiamondChanged() {
    console.log("钻石被换了!");
    this.tips = '钻石被换了!';
  }

  build() {
    Column({ space: 10 }) {
      Text(this.tips)
      Button('replace').onClick(()=>{
        this.box.inner.diamond = '爱心石头';
      })
    }
  }
}

iv. 数组监控的特殊规则

"监控整个书架(数组),换一本书不会报警。必须监控具体书的位置才会报警。"

typescript 复制代码
@Entry
@ComponentV2
export struct  Index{
  @Local books: string[] = ["A", "B", "C"];
  @Local tips:string = '';

  @Monitor('books')                  // ❌ 换一本书不会触发
  onBooksChanged() {
    console.log("整个书架换了");
    this.tips = '整个书架换了';
  }

  @Monitor('books.0')               // ✅ 监控第一本书
  onFirstBookChanged() {
    console.log("第一本书被换了");
    this.tips = '第一本书被换了';

  }

  build() {
    Column({space:10}){
      Text(this.tips);
      Button('换个书架').onClick(()=>{
        this.books = [];
      })
      Button('换第一本书').onClick(()=>{
        this.books[0] = 'Harmony 从入门到放弃';
      })
    }
  }
}

v. 父子摄像头同时工作(继承场景)

"爸爸房间和儿子房间都装了摄像头监控同一个保险箱,保险箱一动,两个摄像头都报警。"

typescript 复制代码
@ObservedV2
class Base {
  @Trace public name: string;

  // 基类监听name属性
  @Monitor('name')
  onBaseNameChange(monitor: IMonitor) {
    console.info('Base Class name change');
  }

  constructor(name: string) {
    this.name = name;
  }
}

@ObservedV2
class Derived extends Base {
  // 继承类监听name属性
  @Monitor('name')
  onDerivedNameChange(monitor: IMonitor) {
    console.info('Derived Class name change');
  }

  constructor(name: string) {
    super(name);
  }
}

@Entry
@ComponentV2
struct Index {
  derived: Derived = new Derived('AAA');

  build() {
    Column() {
      Button('change name')
        .onClick(() => {
          this.derived.name = 'BBB'; // 能够先后触发onBaseNameChange、onDerivedNameChange方法
        })
    }
  }
}

与 @Watch 的对比

特性 @Watch(旧摄像头) @Monitor(新智能摄像头)
写法 @Watch('funcName') 参数 @Monitor('prop') 直接装饰函数
监控能力 基础监控 深度监控(嵌套对象、多维数组)
批量监控 不支持 ✅ 支持监控多个属性
性能 每次变化都触发 同事件多次变化只触发一次

适用场景

  1. 数据验证:监控输入值,超出范围时提示
  2. 自动保存:监控表单变化,自动保存草稿
  3. 日志记录:监控关键数据变化,记录操作日志
  4. 联动更新:监控一个值变化,自动更新其他相关值
  5. 权限检查:监控用户权限变化,更新UI

一句话总结

@Monitor 就是数据的"智能监控摄像头":

  • 监控对象 :必须是状态变量(@Local/@Param 等)
  • 深度透视:能监控嵌套对象的深层属性
  • 批量监控:一个摄像头看多个物品
  • 智能判断:同事件多次变化只报一次警
  • 精确触发 :严格相等(===)判断变化
相关推荐
sam.li11 小时前
鸿蒙HAR对外发布安全流程
安全·华为·harmonyos
sam.li11 小时前
鸿蒙APP安全体系
安全·华为·harmonyos
ChinaDragon14 小时前
HarmonyOS:通过组件导航设置自定义区域
harmonyos
人工智能知识库15 小时前
华为HCIP-HarmonyOS Application Developer题库 H14-231 (26年最新带解析)
华为·harmonyos·hcip-harmonyos·h14-231
搬砖的kk15 小时前
鸿蒙 PC 版 DevEco Studio 使用 OHPM 下载三方库教程
华为·harmonyos
游戏技术分享1 天前
【鸿蒙游戏技术分享 第75期】AGC后台批量导入商品失败,提示“参数错误”
游戏·华为·harmonyos
No Silver Bullet1 天前
HarmonyOS NEXT开发进阶(十七):WebView 拉起 H5 页面
华为·harmonyos
liuhaikang1 天前
【鸿蒙HarmonyOS Next App实战开发】口语小搭档——应用技术实践
harmonyos
liuhaikang1 天前
鸿蒙VR视频播放库——md360player
音视频·vr·harmonyos
飞露1 天前
鸿蒙Preview预览文件失败原因
华为·harmonyos