跳到主要内容

第五章:状态管理 V2(官方 Codelabs 小场景案例)

本章配套代码:code/Chapter5(华为官方 Codelabs《基于状态管理V2-小场景案例》完整移植) 前置要求:已完成第四章组件通信

本章内容严格对照华为官方 Codelabs 教程《基于状态管理V2-小场景案例》(学习时长 30 分钟),每个知识点均为一个可运行的独立小场景,采用「优化前 → 优化后」的对比方式讲解。

5.1 学习目标

  • 掌握 @Local 定义组件内状态
  • 掌握 @Param 接受父组件参数输入
  • 掌握 @Event 实现父子组件通信
  • 掌握 @Provider / @Consumer 跨组件层级共享状态
  • 掌握 @Computed 计算属性与缓存机制
  • 掌握 @Monitor 监听状态变化(单个 / 批量)
  • 掌握 @ObservedV2 / @Trace 管理对象状态(深度观测 / 深度监听)

5.2 状态管理 V2 总览

5.2.1 什么是状态

应用界面并非固定不变,而是会随用户操作动态改变。例如:

  • 滑动开关会改变按钮的点亮状态;
  • 计数器的数值会随每次点击「+1」而增加。

这些需要动态变化并影响界面效果的值,就称为状态(State)

5.2.2 相关概念

装饰器作用
@Local组件内部状态,可对自定义组件中变量变化进行观测,变量变化时刷新使用该变量的组件
@Param接受组件外部输入,还能接受 @Local 的同步变化,变化时刷新关联组件
@Event子组件向父组件请求更新 @Param 变量,装饰回调方法并调用以更新数据源
@Provider/@Consumer跨组件层级数据双向同步,不必拘泥于组件层级
@Computed描述依赖其他状态的计算逻辑并缓存结果,依赖变化时只计算一次
@Monitor监听状态变量变化,变化后执行回调
@ObservedV2/@Trace对类对象属性进行深度观测,增强嵌套类属性变化的观测能力

🔑 @ComponentV2 用于装饰自定义组件。在 @ComponentV2 装饰的组件中,需使用全新的状态变量装饰器(@Local@Param@Once@Event@Provider@Consumer 等)。

5.3 定义组件内状态(@Local)

5.3.1 场景

以一个简单计数器为例:页面显示 value1 / value2 两个值和「-1」「+1」按钮。点击按钮同时修改两个变量,观察差异。

5.3.2 实现原理

使用 @Local 装饰的变量会成为状态变量,发生变化时触发视图刷新;而 public 修饰的普通变量,修改后不会更新组件。

5.3.3 核心代码

对应 code/Chapter5ComponentInternalStatus.ets

@Entry
@ComponentV2
struct ComponentInternalStatus {
@Local value1: number = 0; // 状态变量:变化驱动 UI 刷新
public value2: number = 0; // 普通变量:变化不刷新

build() {
Column({ space: 20 }) {
Text('计数器')
.fontSize(22).fontWeight(FontWeight.Bold)

Row() {
Text('value1').fontSize(22).fontWeight(FontWeight.Bold)
Text(this.value1.toString()) // @Local:点击即刷新
.fontSize(40).fontColor('rgba(10, 89, 247, 1)')
}
.width('90%').borderWidth(1).borderRadius(25)

Row() {
Text('value2').fontSize(22).fontWeight(FontWeight.Bold)
Text(this.value2.toString()) // public:点击不刷新
.fontSize(40).fontColor('rgba(10, 89, 247, 1)')
}
.width('90%').borderWidth(1).borderRadius(25)

Row({ space: 20 }) {
Button('-1').onClick(() => { this.value1--; this.value2-- })
Button('+1').onClick(() => { this.value1++; this.value2++ })
}
}
}
}

运行效果:

@Local 定义组件内状态

5.3.4 总结

需要修改组件内数据并实时显示时,应使用 @Local 修饰变量。详情参考:@Local 装饰器

5.4 接受参数输入(@Param)

5.4.1 场景

两个布局相同的计数器,展示 value1value2。为提升代码复用性,将重复部分封装为 MyCounter 组件。

5.4.2 实现原理

value 变量作用域在 MyCounter 组件内部,而加减按钮在组件外部。@Local 仅支持同步组件内部变量,无法跨组件影响,因此需用 @Param 接收外部传入的状态,确保父子组件数据实时同步。

5.4.3 核心代码

对应 code/Chapter5ParameterInput.ets

@ComponentV2
struct MyCounter {
@Param title: string = ''; // 接收外部传入的标题
@Param value: number = 0; // 接收外部传入的值

build() {
Row() {
Text(`${this.title}`)
.fontSize(22).fontWeight(FontWeight.Bold)
Text(this.value.toString())
.fontSize(40).fontColor('rgba(10, 89, 247, 1)')
}
.width('90%').borderWidth(1).borderRadius(25)
}
}

@Entry
@ComponentV2
struct ParameterInput {
@Local value1: number = 0;
@Local value2: number = 0;

build() {
Column({ space: 20 }) {
Text('计数器')
MyCounter({ title: 'value1', value: this.value1 })
MyCounter({ title: 'value2', value: this.value2 })
Row({ space: 20 }) {
Button('-1').onClick(() => { this.value1--; this.value2-- })
Button('+1').onClick(() => { this.value1++; this.value2++ })
}
}
}
}

运行效果:

@Param 接受参数输入

5.4.4 总结

定义大量重复组件或样式时,封装成自定义组件,内部用 @Param 修饰变量以接收调用时传入的参数。详情参考:@Param 装饰器

5.5 父子组件通信(@Event)

5.5.1 场景

创建 ParentChild 组件,在 Child 中点击按钮需要修改从父组件传入的 index 值。

5.5.2 实现原理

@Param 接收的变量在子组件内不可直接修改,更新需在父组件完成。子组件可借助 @Event 定义方法向父组件发出更新请求,父组件响应后,新数据通过 @Param 再次同步回子组件,实现状态间接更新。

5.5.3 核心代码

对应 code/Chapter5EventCommunication.ets

@ComponentV2
struct EventChild {
@Param index: number = 0;
@Event changeIndex: (val: number) => void; // 向父组件请求更新

build() {
Column({ space: 10 }) {
Text('Child').fontSize(18).fontWeight(700)
Text(this.index.toString()).fontSize(36).fontWeight(700)
Button('Change to 20')
.onClick(() => {
this.changeIndex(20); // 调用 @Event,触发父组件更新
})
}
.width(260).padding(16).backgroundColor(Color.White).borderRadius(25)
}
}

@Entry
@ComponentV2
struct EventParent {
@Local index: number = 0;

build() {
Column() {
Text('Parent').fontSize(18).fontWeight(700)
EventChild({
index: this.index,
changeIndex: (val: number) => {
this.index = val; // 父组件实现更新逻辑
}
})
}
.width(292).backgroundColor('rgba(10,89,247,0.1)').borderRadius(25)
}
}

运行效果:

@Event 父子组件通信

5.5.4 总结

子组件需修改 @Param 数据时,应定义方法并用 @Event 装饰,在父组件中实现具体功能。详情参考:@Event 装饰器

5.6 跨组件共享状态(@Provider / @Consumer)

5.6.1 场景

在多层级嵌套场景中,为把数据/事件传至最内层组件,需通过中间组件逐层转发,导致中间组件承担无关参数、代码臃肿。例如在 ParentChild 间添加 ChildList 组件。

5.6.2 实现原理

深层级嵌套时应使用 @Provider / @Consumer 优化。父组件用 @Provider 装饰变量或事件,作为所有后代组件的依赖提供者;无论后代层级多深,都可用 @Consumer 通过相同 key 接收,不受组件层级限制。

5.6.3 核心代码(状态同步优化后)

对应 code/Chapter5StateSynchronizationAfter.ets

@ComponentV2
struct SSAfterChild {
@Consumer('index') index: number = 0; // 通过 key 获取最近父节点的 @Provider

build() {
Column() {
Text('Child')
Text(this.index.toString()).fontSize(36)
}
.width(260).backgroundColor(Color.White).borderRadius(25)
}
}

@ComponentV2
struct SSAfterChildList {
// 无需声明 index 变量及传参,直接复用 Child
build() {
Column() {
Text('ChildList')
SSAfterChild()
SSAfterChild()
SSAfterChild()
}
.width(292).backgroundColor('rgba(10,89,247,0.1)').borderRadius(25)
}
}

@Entry
@ComponentV2
struct SSAfterParent {
@Provider('index') index: number = 0; // 提供跨层数据

build() {
Column() {
Text('Parent')
SSAfterChildList()
}
.width(340).backgroundColor('rgba(10,89,247,0.05)').borderRadius(25)
}
}

💡 事件同步同样可用 @Provider / @Consumer 优化:用 @Provider('changeIndex') 装饰方法,子组件用 @Consumer('changeIndex') 接收(见 EventSynchronizationAfter.ets)。

运行效果:

@Provider/@Consumer 跨组件共享

5.6.4 总结

@Provider / @Consumer 能达到与 @Param / @Event 相同的效果,且层级较深时可省略大量中间组件传递,层级较深时优先使用。详情参考:@Provider/@Consumer 装饰器

5.7 使用计算属性(@Computed)

5.7.1 场景

一个苹果计价器:点击「-1个」「+1个」控制苹果数量,金额随数量同步更新。若在每个按钮事件中都写金额计算逻辑,会导致代码重复臃肿。

5.7.2 实现原理

@Computed 装饰器描述计算逻辑并缓存结果,只有当方法内参与计算的相关变量变化时才重新计算,提高性能与效率。

5.7.3 核心代码

对应 code/Chapter5StateCacheAfter.ets

@Entry
@ComponentV2
struct StateCacheAfter {
@Local count: number = 0;
price: number = 15;

@Computed
get getTotal(): number { // 依赖 count,变化时自动重算
return this.count * this.price;
}

build() {
Column({ space: 18 }) {
Text('苹果').fontSize(22).fontWeight(FontWeight.Bold)
Text('单价' + this.price).fontSize(16)

Row() {
Column() {
Text(this.getTotal.toString()) // 直接调用计算属性
.fontSize(42).fontWeight(FontWeight.Bold)
Text('总价')
Button('-1个').onClick(() => {
if (this.count > 0) { this.count--; }
})
}
Column() {
Text(this.count.toString()).fontSize(42).fontWeight(FontWeight.Bold)
Text('数量')
Button('+1个').onClick(() => { this.count++; })
}
}
}
}
}

运行效果:

@Computed 计算属性

5.7.4 总结

UI 上重复使用相同计算逻辑时,用 @Computed 提取计算逻辑,便于统一维护。详情参考:@Computed 装饰器

5.8 监听状态变化(@Monitor)

5.8.1 场景

点击修改 index 值的按钮后,需获取 index 变化前后的值。

5.8.2 实现原理

@Monitor 装饰器监听状态变量变化,当被装饰的变量变化时,其定义的回调方法被调用。回调参数 IMonitor 实例包含变化状态及变化前后的值。

5.8.3 核心代码(单个 / 批量)

对应 code/Chapter5Monitoring.ets(单个)与 MonitoringBatch.ets(批量):

// 单个监听
@Entry
@ComponentV2
struct Monitoring {
@Local index: number = 0;
@Local change: string = '';

@Monitor('index')
indexChange(monitor: IMonitor) {
this.change = `index change from ${monitor.value()?.before} to ${monitor.value()?.now}`;
}

build() {
Column({ space: 20 }) {
Button(`Change index ${this.index}`)
.onClick(() => { this.index = 20; })
Text(this.change).fontSize(19)
}
}
}

// 批量监听:monitor.dirty 获取变化属性路径数组
@Entry
@ComponentV2
struct MonitoringBatch {
@Local index1: number = 0;
@Local index2: number = 0;
@Local change: string = '';

@Monitor('index1', 'index2')
indexChange(monitor: IMonitor) {
monitor.dirty.forEach((path: string) => {
this.change = `${path} change from ${monitor.value(path)?.before} to ${monitor.value(path)?.now}`;
})
}
}

运行效果:

@Monitor 监听状态变化

5.8.4 总结

需在变量变化时获取前后值并执行操作时,用 @Monitor 监听。详情参考:@Monitor 装饰器

5.9 管理对象状态(@ObservedV2 / @Trace)

5.9.1 场景

有一个学生对象,包含 name(姓名)、age(年龄)、major(专业)三个属性,其中 major 来源于 Major 对象的 className 属性。点击按钮把 className 由「软件工程」改为「物联网」。

5.9.2 实现原理

仅用 @Local 定义对象,无法观测到对象内部属性的修改。需用 @ObservedV2 装饰类、@Trace 装饰属性,使类及其属性具有深度观测能力,甚至支持嵌套类结构。

5.9.3 核心代码

对应 code/Chapter5StatusOfManagementObjectsAfter.ets

@ObservedV2
class MajorAfter {
@Trace className: ResourceStr = '';
}

class StudentAfter {
name: ResourceStr = '';
age: number = 17;
major: MajorAfter = new MajorAfter();

constructor(name: ResourceStr, age: number, className: ResourceStr) {
this.name = name;
this.age = age;
this.major.className = className;
}
}

@Entry
@ComponentV2
struct StatusOfManagementObjectsAfter {
@Local student: StudentAfter = new StudentAfter('李华', 18, '软件工程');

// 深度监听嵌套属性
@Monitor('student.major.className')
majorChange(monitor: IMonitor) {
console.log(`student.major.className change from ${monitor.value()?.before} to ${monitor.value()?.now}`);
}

build() {
Column() {
Text(this.student.name) // 李华
Text(this.student.age + '岁') // 18岁
Text(this.student.major.className) // 专业:软件工程 → 物联网
Button('专业改为物联网')
.onClick(() => { this.student.major.className = '物联网'; })
}
}
}

运行效果:

@ObservedV2/@Trace 管理对象状态

5.9.4 总结

复杂变量类型用 @ObservedV2 + @Trace 对其内部属性进行观测;配合 @Monitor 还可实现深度监听。详情参考:@ObservedV2/@Trace 装饰器

5.10 综合导航页

本章代码移植官方 Codelabs 全部 14 个小场景(每个 .ets 文件实现一个案例),并新增 Index.ets 导航页统一入口,点击可跳转查看各案例:

状态管理 V2 案例导航页

案例文件知识点
ComponentInternalStatus.ets@Local 组件内状态
ParameterInput.ets@Param 参数输入
EventCommunication.ets@Event 父子通信
StateSynchronizationBefore/After.ets跨组件状态同步(优化前后)
EventSynchronizationBefore/After.ets跨组件事件同步(优化前后)
StateCacheBefore/After.ets@Computed 计算属性(优化前后)
Monitoring.ets / MonitoringBatch.ets@Monitor 监听(单个/批量)
StatusOfManagementObjectsBefore/After.ets@ObservedV2/@Trace 对象状态(优化前后)

5.11 本章小结

知识点说明
@Local组件内部状态,变化驱动 UI 刷新
@Param接收外部传入的参数,实现父传子
@Event子组件向父组件请求更新数据
@Provider/@Consumer跨组件层级双向同步(状态 + 事件)
@Computed计算属性缓存,依赖变化才重算
@Monitor监听状态变化,获取变化前后值
@ObservedV2/@Trace类对象属性深度观测与深度监听

5.12 课后练习

  1. ComponentInternalStatus 基础上,用 @Local 实现一个「购物车数量」计数器
  2. @Param 封装一个可复用的「价格展示」组件(接收价格和货币符号)
  3. @Event 实现子组件删除按钮,删除父组件列表中的某一条数据
  4. @Provider/@Consumer 实现全局用户信息(昵称、头像)跨层共享
  5. @Computed 实现购物车总价计算(单价 × 数量 × 折扣)
  6. @Monitor 监听搜索关键词变化,实时过滤列表
  7. @ObservedV2/@Trace 定义 Address 类,测试嵌套属性修改的 UI 刷新

5.13 参考资料

评论

加载中…
加载中...