一、@State 存在的问题

在 HarmonyOS 状态管理 V1 中,@State 存在一个关键缺陷:@State装饰的变量既可以从父组件传入初始值,也可以在组件内部初始化,这会导致:

  1. 外部覆盖内部初始值。父组件可以在子组件初始化时传入新值,这会直接覆盖子组件内部 @State定义的初值。例如:

    class ComponentInfo {
      name: string;
      id: number;
      message: string;
    
      constructor(name:string, id:number, message:string) {
        this.name = name;
        this.id = id;
        this.message = message;
      }
    }
    
    @Entry
    @Component
    struct Index {
      build() {
        Column() {
          // 父组件调用时传入新值,子组件内部定义的初值被覆盖
          Child({ componentInfo: new ComponentInfo('Unknown', 0, 'Error') })
        }
      }
    }
    
    @Component
    struct Child {
      @State componentInfo: ComponentInfo = new ComponentInfo('Child', 1, 'Hello World');
    
      build() {
        Column() {
          Text(JSON.stringify(this.componentInfo)).fontSize(30)
        }
      }
    }
    

    运行效果:

    在这里插入图片描述

  2. 组件无法感知外部初始化。虽然外部覆盖了内部值,但子组件本身无法感知componentInfo 是从外部传入还是内部定义的。这种不确定性破坏了组件的封装性,不利于状态的管理和维护。

  3. 状态来源不唯一。一个变量既可能来自组件自身(内部状态),也可能来自外部传入(类似输入参数)。这种语义模糊性导致组件职责不清晰,容易出现数据流混乱的问题。

二、@Local(V2 状态管理)

随着鸿蒙系统的演进,推出了 V2 状态管理。在 V2 中,不再使用 @State,而是改用 @Local 关键字。@Local 是 V2 状态管理的装饰器,功能与 @State 类似,但设计更完善。

将上面的代码中的 @State 改成 @Local

@ComponentV2
struct Child {
  @Local componentInfo: ComponentInfo = new ComponentInfo('Child', 1, 'Hello World');
  //...
}

运行效果:

在这里插入图片描述

从图中可以看到报错了。外部无法通过父组件给子组件 @Local 修饰的变量传值,确保了数据来源唯一,保证该状态完全由组件自身管理。

2.1 使用 @Local 修饰基本数据类型

@Entry
@ComponentV2
struct Index {
  @Local count: number = 0
  build() {
    Text(this.count + "")
      .onClick(() => {
        this.count++
        console.log(this.count + "")
      })
  }
}

注意:要想使用V2状态管理,需要把 @Component 改为 @ComponentV2

2.2 使用 @Local 修饰对象

在 V2 状态管理中,@Local 修饰对象时:只观测变量本身的赋值,即当整个对象被重新赋值时,才会触发 UI 更新。对于对象内部第一层以及更深层属性的直接修改,@Local 默认并不会检测到。

interface Pet {
  name: string,
  age: number
}

interface Person {
  name: string,
  age: number,
  pet: Pet
}

@Entry
@ComponentV2
struct ObjectLocalDemo {
  @Local person: Person = {
    name: '张三',
    age: 25,
    pet: {
      name: '旺财',
      age: 2
    }
  }

  build() {
    Column({ space: 16 }) {
      Text(`姓名:${this.person.name}`)
        .fontSize(20)
      Text(`年龄:${this.person.age}`)
        .fontSize(20)
      Text(`宠物名:${this.person.pet.name}`)
        .fontSize(20)
      Text(`宠物年龄:${this.person.pet.age}`)
        .fontSize(20)

      Button('修改人名')
        .onClick(() => {
          this.person.name = '李四';
        })

      Button('修改年龄')
        .onClick(() => {
          this.person.age = 30;
        })

      Button('修改宠物名')
        .onClick(() => {
          this.person.pet.name = '咪咪';
        })

      Button('修改整个对象')
        .onClick(() => {
          this.person = {
            name: '王五',
            age: 35,
            pet: {
              name: '小黑',
              age: 3
            }
          };
        })
    }
    .width('100%')
    .padding(20)
  }
}

2.2.1 运行效果分析

点击 修改人名 按钮,从日志中可以看到人名确实修改成功了,但页面上的姓名并没有变化。这是因为 @Local 只观测变量本身的赋值(即整个对象被重新赋值),而 this.person.name = '李四' 是对对象内部属性的修改,@Local 无法检测到。

点击 修改年龄 按钮,同样,年龄虽然修改成功,但页面上的年龄并没有变化,原因同上。

点击 修改宠物名 按钮,从日志中可以看到宠物名确实修改成功了,但页面上的宠物名也没有变化this.person.pet.name = '咪咪' 是深层属性的直接修改,@Local 同样无法检测到。

在这里插入图片描述

点击 修改整个对象 按钮,页面上的所有数据都更新了(整个对象被重新赋值,@Local 能检测到)。

2.2.2 与 @State 的对比

对比项 @State(V1) @Local(V2)
组件装饰器 @Component @ComponentV2
整个对象重新赋值 ✅ 触发 UI 更新 ✅ 触发 UI 更新
第一层属性直接修改 ✅ 触发 UI 更新 ❌ 不触发 UI 更新
深层嵌套属性直接修改 ❌ 不触发 UI 更新 ❌ 不触发 UI 更新
修改深层属性方式 需重新赋值第一层属性 需重新赋值整个对象或使用 @ObservedV2 + @Trace

三、使用 @ObservedV2 + @Trace 实现深层属性观测

从上面的对比表可以看到,@Local 无法检测对象内部属性的直接修改。为了解决这个问题,V2 状态管理提供了 @ObservedV2@Trace 装饰器,它们配合使用可以实现对对象深层属性的精确观测。

3.1 基本概念

  • @ObservedV2:装饰在 class 上,表示该类是一个可观测的类,其内部被 @Trace 装饰的属性变化会被追踪。
  • @Trace:装饰在 class 内部的属性上,表示该属性是一个可观测的状态变量,当其值发生变化时,会触发关联的 UI 更新。

注意:

  1. 类本身必须使用 @ObservedV2装饰,单独使用 @Trace 无效。
  2. 未被 @Trace装饰的属性无法触发 UI 刷新(即使类被 @ObservedV2装饰)。

3.2 使用 @ObservedV2 + @Trace 改造对象嵌套示例

下面我们用 @ObservedV2@Trace 改造之前的 Person 对象示例,让深层属性修改也能触发 UI 更新:

// 使用 @ObservedV2 装饰类,使其成为可观测类
@ObservedV2
class Pet {
  @Trace name: string;
  @Trace age: number;

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

@ObservedV2
class Person {
  @Trace name: string;
  @Trace age: number;
  @Trace pet: Pet;

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

@Entry
@ComponentV2
struct ObservedV2Demo {
  @Local person: Person = new Person('张三', 25, new Pet('旺财', 2));

  build() {
    Column({ space: 16 }) {
      Text(`姓名:${this.person.name}`)
        .fontSize(20)
      Text(`年龄:${this.person.age}`)
        .fontSize(20)
      Text(`宠物名:${this.person.pet.name}`)
        .fontSize(20)
      Text(`宠物年龄:${this.person.pet.age}`)
        .fontSize(20)

      Button('修改人名')
        .onClick(() => {
          this.person.name = '李四';
        })

      Button('修改年龄')
        .onClick(() => {
          this.person.age = 30;
        })

      Button('修改宠物名')
        .onClick(() => {
          this.person.pet.name = '咪咪';
        })

      Button('修改宠物年龄')
        .onClick(() => {
          this.person.pet.age = 3;
        })

      Button('修改整个对象')
        .onClick(() => {
          this.person = new Person('王五', 35, new Pet('小黑', 3));
        })
    }
    .width('100%')
    .padding(20)
  }
}

3.2.1 运行效果分析

运行效果:

在这里插入图片描述

点击 修改人名 按钮,页面上的姓名立即更新为"李四"。因为 Person 类被 @ObservedV2 装饰,且 name 属性被 @Trace 装饰,所以 this.person.name = '李四' 这个直接修改能被检测到并触发 UI 更新。

点击 修改年龄 按钮,页面上的年龄立即更新为 30,原因同上。

点击 修改宠物名 按钮,页面上的宠物名立即更新为"咪咪"。注意,这里修改的是 this.person.pet.name,属于深层嵌套属性。因为 Pet 类也被 @ObservedV2 装饰,且 name 属性被 @Trace 装饰,所以深层属性的修改也能被检测到。

点击 修改宠物年龄 按钮,页面上的宠物年龄立即更新为 3,同样能触发 UI 更新。

点击 修改整个对象 按钮,页面上的所有数据都更新了,这与 @Local 的行为一致。

从上面的示例可以看出,@Local 配合@ObservedV2 + @Trace 可以实现对整个对象以及对象深层属性的监听,比使用 @State 配合 @Observed 的要方便很多。

3.2.2 与 @State + @Observed 的对比

对比项 @State + @Observed(V1) @Local + @ObservedV2 + @Trace(V2)
组件装饰器 @Component @ComponentV2
类装饰器 @Observed @ObservedV2
属性装饰器 无(默认观测所有属性) @Trace(需手动标记)
整个对象重新赋值 ✅ 触发 UI 更新 ✅ 触发 UI 更新
第一层属性直接修改 ✅ 触发 UI 更新 ✅ 触发 UI 更新
深层嵌套属性直接修改 ✅ 触发 UI 更新 ✅ 触发 UI 更新
性能优化 默认观测所有属性,性能开销较大 仅观测被 @Trace 标记的属性,性能更优

3.2.3 使用建议

  1. 简单场景:如果只是单个基本类型变量(如 numberstring),直接用 @Local 即可。
  2. 对象场景:如果对象内部属性需要被直接修改并触发 UI 更新,建议使用 @ObservedV2 + @Trace
  3. 性能敏感场景@ObservedV2 + @Trace 比 V1 的 @Observed 性能更好,因为它是按需观测,只追踪被 @Trace 标记的属性。
  4. 嵌套对象场景:每一层嵌套的类都需要用 @ObservedV2 装饰,且需要被观测的属性都要用 @Trace 装饰。

四、总结

V2 状态管理提供了更灵活、更精细的状态控制能力:

  • @Local:用于组件级别的状态变量声明,观测变量本身的赋值。
  • @ObservedV2:装饰 class,使其成为可观测类。
  • @Trace:装饰 class 内部的属性,标记该属性为可观测状态。

三者配合使用,可以实现对对象深层属性的精确观测,同时保持较好的性能表现。在开发鸿蒙应用时,建议优先使用 V2 状态管理方案。

Logo

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

更多推荐