一、状态管理V2概述
1、V1到V2的演进:为什么需要状态管理V2?
比喻:小区快递管理
假设你的 App 是一个小区 ,数据是住户的快递 ,UI 是快递柜。
状态管理 V1(老系统)
工作方式: 给每个快递柜配一个专属的快递员(代理观察者),快递员只看自己柜子里的快递单,不看住户本人。
问题:
-
住户搬家不知道(数据不独立)
- 你把快递从"1号楼301"改到"2号楼502",但只告诉了快递员A。
- 快递员B还在傻等"1号楼301"的快递,导致其他快递柜显示错误。
-
看不清快递内容(深度观察不了)
- 快递员只看外包装标签,如果快递里是一个包裹套包裹(对象嵌套对象),里面的小包裹换了,快递员发现不了。
-
整柜换新(冗余更新)
- 住户只是换了一个小物品,但快递员嫌麻烦,把整个快递柜清空重放,导致刷新很慢。
-
职责不清(组件化难)
- 快递员、快递柜、住户关系混乱,谁该管什么不清楚,很难模块化管理。
状态管理 V2(新系统)
工作方式: 不再给柜子配快递员,而是给每个快递本身装上 GPS 追踪芯片。快递去哪,系统自动通知所有相关快递柜。
改进:
-
快递独立(数据独立)
- 快递(数据)本身带有追踪能力,无论放到哪个快递柜,都能自动更新显示。
- ✅ 数据真正和 UI 解耦。
-
透视眼(深度观察)
- 哪怕快递里套了 10 层小包裹,最里面换了个小零件,系统也能精准感知,并通知到相关快递柜。
- ✅ 性能还更好。
-
精准更换(最小化更新)
- 住户只换了一个小物品,系统就只更新那个小格子,其他格子不动。
- ✅ 更新更快、更省资源。
-
职责分明(组件化易)
- 系统明确区分:这是输入的快递 (
@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也会反映到这里
})
}
}
}
核心要点总结
- 单向同步(主要):父 → 子(父更新,子自动更新)
- 双向修改(特殊情况) :如果是对象/数组,子组件可以修改内部属性,父组件也能看到
- 不能直接赋值 :子组件不能
this.param = 新值 - 支持所有类型:字符串、数字、对象、数组等都能传
- 自动刷新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?
想象以下实际场景:
- 版本信息:App版本号只需要初始化时传一次,后续更新不需要同步
- 用户首次选择:用户第一次选择的配置,后续父组件改了也不影响子组件
- 快照数据:需要保留某个时刻的数据快照
- 性能优化:明确知道某些数据只需要传一次,避免不必要的同步开销
核心要点总结
- 黄金搭档 :必须和
@Param一起使用 - 一次性接收:只收父组件的第一次传值
- 本地自由:子组件可以随意修改这个值
- 单向隔离:父组件后续更新影响不到子组件
- 顺序灵活 :
@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)
}
}
}
实际应用场景
- 表单组件:子组件收集输入,通过 @Event 提交给父组件
- 弹窗组件:通过 @Event 通知父组件用户点击了确定/取消
- 列表项:每个列表项通过 @Event 通知父组件被点击了
- 自定义按钮:按钮通过 @Event 通知点击事件
核心要点总结
- 解决限制:@Param 不能直接改 → 用 @Event 请求父组件改
- 单向通信:子 → 父(与 @Param 的父 → 子相反)
- 必须是函数:只能装饰回调函数类型
- 默认值支持:可以设置本地默认函数
- 清晰接口:明确组件的输入(@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"; // 这里修改,所有地方都变!
})
}
}
使用注意事项(重要!)
⚠️ 强依赖层级(家族辈分)
"你的位置(组件层级)决定了你能收到哪个电台的信号。"
⚠️ 减少使用(保持独立)
"就像不要用胶水把全家人都粘在一起,尽量保持组件独立,只在必要时用广播系统。"
✅ 适用场景
- 主题切换:所有组件都要同步主题色
- 用户登录状态:全局共享登录信息
- 多语言:全局语言设置
- 全局配置:如字体大小、深色模式
❌ 不适用场景
- 父子组件简单传值:用 @Param 更合适
- 独立组件:不需要广播的组件
与@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') 直接装饰函数 |
| 监控能力 | 基础监控 | 深度监控(嵌套对象、多维数组) |
| 批量监控 | 不支持 | ✅ 支持监控多个属性 |
| 性能 | 每次变化都触发 | 同事件多次变化只触发一次 |
适用场景
- 数据验证:监控输入值,超出范围时提示
- 自动保存:监控表单变化,自动保存草稿
- 日志记录:监控关键数据变化,记录操作日志
- 联动更新:监控一个值变化,自动更新其他相关值
- 权限检查:监控用户权限变化,更新UI
一句话总结
@Monitor 就是数据的"智能监控摄像头":
- 监控对象 :必须是状态变量(
@Local/@Param等) - 深度透视:能监控嵌套对象的深层属性
- 批量监控:一个摄像头看多个物品
- 智能判断:同事件多次变化只报一次警
- 精确触发 :严格相等(
===)判断变化