在 HarmonyOS 应用开发中,状态管理是 UI 刷新的核心机制。如果你用过 @State 装饰器,可能会遇到组件内部状态被外部意外修改的问题。今天咱们就来聊聊 API version 12 开始支持的 @Local 装饰器,这个专门为 @ComponentV2 组件设计的内部状态管理工具,到底有啥过人之处。

为啥需要 @Local?先说说 @State 的小麻烦

在介绍 @Local 之前,咱们先看看老伙计 @State 的一个痛点。@State 虽然能管理组件状态,但它允许从外部初始化,这就可能导致组件内部状态被意外篡改。

重点总结

  • @State 允许外部初始化组件状态变量,破坏内部状态独立性
  • 组件无法感知外部对内部状态的修改,不利于状态管理

看看这段代码就明白了:

class ComponentInfo {
  name: string;
  count: number;
  message: string;
  constructor(name: string, count: number, message: string) {
    this.name = name;
    this.count = count;
    this.message = message;
  }
}

@Component
struct Child {
  // 本意是作为内部状态的变量
  @State componentInfo: ComponentInfo = new ComponentInfo("Child", 1, "Hello World");
  build() {
    Column() {
      Text(`componentInfo.message is ${this.componentInfo.message}`)
    }
  }
}

@Entry
@Component
struct Index {
  build() {
    Column() {
      // 外部直接修改了Child的内部状态
      Child({ componentInfo: new ComponentInfo("Unknown", 0, "Error") })
    }
  }
}

这段代码里,Child 组件原本想把 componentInfo 作为内部状态,结果被父组件 Index 在初始化时直接覆盖了。这种情况会让组件状态变得不可控,所以 @Local 装饰器应运而生,专门解决这个问题。

@Local 装饰器基础用法:组件的 "私有财产"

@Local 最核心的特点就是:声明的变量是组件的私有财产,必须在内部初始化,外部谁也别想插手

重点总结

  • 仅能在 @ComponentV2 装饰的组件中使用
  • 变量必须本地初始化,禁止外部传入初始值
  • 支持多种数据类型,包括基本类型和复杂类型
  • 变量变化时会自动刷新关联的 UI

先看个正确用法的例子:

// 正确用法:在@ComponentV2中使用,本地初始化
@ComponentV2
struct MyComponent {
  @Local message: string = "Hello World"; // 必须本地初始化
  build() {
    Text(this.message)
  }
}

再看看哪些用法会踩坑:

// 错误用法1:在普通@Component中使用@Local
@Component
struct TestComponent {
  @Local message: string = "Hello World"; // 编译报错
  build() {}
}

// 错误用法2:从外部初始化@Local变量
@ComponentV2
struct ChildComponent {
  @Local message: string = "Hello World";
  build() {}
}

@ComponentV2
struct ParentComponent {
  build() {
    // 试图从外部传值给@Local变量,编译报错
    ChildComponent({ message: "Hello" }) 
  }
}

记住这两条铁律:@Local 只认 @ComponentV2,初始化只能自己来。

@Local 能观测哪些变化?不同类型有讲究

@Local 对不同数据类型的观测能力不太一样,咱们分类型来说说,这部分特别重要,直接影响你写代码的逻辑。

1. 基本类型(number/string/boolean)

重点总结

  • 支持直接观测赋值变化
  • 简单修改(如自增、拼接)都会触发 UI 刷新

代码示例一目了然:

@Entry
@ComponentV2
struct BasicTypeDemo {
  @Local count: number = 0;
  @Local message: string = "Hello";
  @Local flag: boolean = false;

  build() {
    Column() {
      Text(`数字: ${this.count}`)
      Text(`字符串: ${this.message}`)
      Text(`布尔值: ${this.flag}`)
      
      Button("修改数据")
        .onClick(() => {
          this.count++; // 数字变化会被观测
          this.message += " World"; // 字符串拼接会被观测
          this.flag = !this.flag; // 布尔值反转会被观测
        })
    }
  }
}

点击按钮后,三个 Text 都会实时刷新,因为基本类型的赋值操作能被 @Local 直接捕捉。

2. 类对象(class)

重点总结

  • 直接修改对象的属性不会触发刷新
  • 只有对对象整体重新赋值才会触发刷新
  • 若要观测属性变化,需要配合 @ObservedV2 和 @Trace

先看个不生效的例子:

// 普通类,没有观测能力
class RawObject {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

@Entry
@ComponentV2
struct ObjectDemo1 {
  @Local rawObj: RawObject = new RawObject("原始值");

  build() {
    Column() {
      Text(this.rawObj.name)
      Button("修改属性")
        .onClick(() => {
          // 直接修改属性,不会触发UI刷新
          this.rawObj.name = "修改后的值"; 
        })
      Button("整体赋值")
        .onClick(() => {
          // 整体重新赋值,会触发UI刷新
          this.rawObj = new RawObject("新对象"); 
        })
    }
  }
}

想让对象属性的修改被观测到,得给类加 @ObservedV2,属性加 @Trace:

// 带观测能力的类
@ObservedV2 // 标记类支持观测
class ObservedObject {
  @Trace name: string; // 标记属性需要被追踪
  constructor(name: string) {
    this.name = name;
  }
}

@Entry
@ComponentV2
struct ObjectDemo2 {
  @Local observedObj: ObservedObject = new ObservedObject("初始值");

  build() {
    Column() {
      Text(this.observedObj.name)
      Button("修改属性")
        .onClick(() => {
          // 因为加了@Trace,属性修改会触发刷新
          this.observedObj.name = "属性变了"; 
        })
    }
  }
}

这里要注意:@Local 不能和 @Observed(V1 版本)混用,认准 @ObservedV2 和 @Trace。

3. 数组(Array)

重点总结

  • 支持观测数组整体赋值
  • 支持观测数组元素的直接修改(如 arr [0] = 1)
  • 支持观测数组 API 调用(push/pop/splice 等)
  • 嵌套数组(如二维数组)的元素变化也能观测

代码示例包含各种数组操作:

@Entry
@ComponentV2
struct ArrayDemo {
  @Local numArr: number[] = [1, 2, 3];
  @Local twoDArr: number[][] = [[10, 20], [30, 40]];

  build() {
    Column() {
      // 展示一维数组
      Text("一维数组: " + this.numArr.join(","))
      // 展示二维数组
      Text("二维数组: " + this.twoDArr.map(row => row.join(",")).join(";"))
      
      Button("修改元素")
        .onClick(() => {
          this.numArr[0] = 100; // 直接修改元素
          this.twoDArr[0][1] = 200; // 修改二维数组元素
        })
      
      Button("数组API操作")
        .onClick(() => {
          this.numArr.push(4); // push操作会被观测
          this.twoDArr.splice(1, 1); // splice操作会被观测
        })
      
      Button("整体替换")
        .onClick(() => {
          this.numArr = [5, 6, 7]; // 整体赋值
        })
    }
  }
}

数组这块 @Local 表现不错,不管是改元素还是用 API,都能准确捕捉变化。

4. 嵌套对象数组

重点总结

  • 数组元素是对象时,对象属性修改需要 @Trace
  • 数组整体替换、元素替换会被观测
  • 深层属性(如对象的对象)修改需多层 @Trace

看个复杂点的嵌套例子:

// 深层嵌套的观测类
@ObservedV2
class Region {
  @Trace x: number; // 需@Trace才能观测
  @Trace y: number;
  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}

@ObservedV2
class Info {
  @Trace name: string; // 第一层属性
  @Trace region: Region; // 嵌套对象
  constructor(name: string, x: number, y: number) {
    this.name = name;
    this.region = new Region(x, y);
  }
}

@Entry
@ComponentV2
struct NestedArrayDemo {
  @Local infoArr: Info[] = [
    new Info("海洋", 28, 120),
    new Info("山脉", 26, 20)
  ];

  build() {
    Column() {
      ForEach(this.infoArr, (info: Info) => {
        Row() {
          Text(`名称: ${info.name}`)
          Text(`坐标: ${info.region.x},${info.region.y}`)
        }
      })
      
      Button("修改名称")
        .onClick(() => {
          this.infoArr[0].name = "湖泊"; // @Trace生效,会刷新
        })
      
      Button("修改坐标")
        .onClick(() => {
          this.infoArr[0].region.x = 30; // 深层@Trace生效
        })
    }
  }
}

只要嵌套层级的属性都加了 @Trace,不管多深的修改都能被观测到。

5. 内置类型(Date/Map/Set)

这些类型比较特殊,除了整体赋值,它们的 API 调用也能被观测到,咱们一个个说。

Date 类型

可观测的 API:setFullYear、setMonth、setDate、setHours 等所有设置时间的方法

@Entry
@ComponentV2
struct DateDemo {
  @Local selectedDate: Date = new Date('2021-08-08');

  build() {
    Column() {
      Text(`当前日期: ${this.selectedDate.toLocaleDateString()}`)
      
      Button("年份+1")
        .onClick(() => {
          this.selectedDate.setFullYear(this.selectedDate.getFullYear() + 1);
        })
      
      Button("月份+1")
        .onClick(() => {
          this.selectedDate.setMonth(this.selectedDate.getMonth() + 1);
        })
      
      Button("直接换日期")
        .onClick(() => {
          this.selectedDate = new Date('2023-07-08'); // 整体赋值
        })
    }
  }
}
Map 类型

可观测的 API:set、clear、delete

@Entry
@ComponentV2
struct MapDemo {
  @Local myMap: Map<number, string> = new Map([[0, "a"], [1, "b"]]);

  build() {
    Column() {
      ForEach(Array.from(this.myMap.entries()), ([key, value]) => {
        Text(`${key}: ${value}`)
      })
      
      Button("添加元素")
        .onClick(() => this.myMap.set(2, "c"))
      
      Button("清空")
        .onClick(() => this.myMap.clear())
      
      Button("删除元素")
        .onClick(() => this.myMap.delete(0))
    }
  }
}
Set 类型

可观测的 API:add、clear、delete

@Entry
@ComponentV2
struct SetDemo {
  @Local mySet: Set<number> = new Set([0, 1, 2]);

  build() {
    Column() {
      ForEach(Array.from(this.mySet), (item) => {
        Text(item.toString())
      })
      
      Button("添加元素")
        .onClick(() => this.mySet.add(3))
      
      Button("清空")
        .onClick(() => this.mySet.clear())
    }
  }
}

这些内置类型的 API 调用之所以能被观测,是因为 @Local 给它们加了一层代理,专门监控这些方法的调用。

6. 联合类型(包括 null 和 undefined)

@Local 还支持联合类型,比如 number | undefined,切换类型也能触发刷新:

@Entry
@ComponentV2
struct UnionTypeDemo {
  @Local count: number | undefined = 10;

  build() {
    Column() {
      Text(`当前值: ${this.count ?? 'undefined'}`)
      
      Button("切换为undefined")
        .onClick(() => this.count = undefined)
      
      Button("切换为数字")
        .onClick(() => this.count = 20)
    }
  }
}

点击按钮时,Text 会根据 count 的类型变化实时更新。

@Local vs @State:到底该用哪个?

很多同学会问,既然有 @State 了,为啥还要 @Local?咱们用表格对比一下,一目了然:

特性 @State @Local
初始化来源 可以从外部传入 必须本地初始化,禁止外部传入
观测能力 能观测变量本身和一层属性 只观测变量本身,深层依赖 @Trace
适用组件 @Component 和 @ComponentV2 都能用 只能在 @ComponentV2 中使用
状态独立性 可能被外部修改,不够纯粹 完全内部状态,不受外部影响
数据传递 可作为数据源同步给子组件 可作为数据源同步给子组件

简单说:如果这个状态完全属于组件自己,不希望被外部干扰,选 @Local;如果需要从父组件传初始值,用 @State。

@Local 的典型使用场景

除了基础用法,@Local 在某些场景下特别有用,比如:

场景 1:确保组件内部状态不被外部篡改

这是 @Local 的核心价值,比如写一个计数器组件,不希望外部能随便改初始值:

@ComponentV2
struct Counter {
  @Local count: number = 0; // 确保初始值只能是0
  
  build() {
    Column() {
      Text(`计数: ${this.count}`)
      Button("+1").onClick(() => this.count++)
    }
  }
}

// 父组件无法修改Counter的初始count
@Entry
@ComponentV2
struct Parent {
  build() {
    Counter() // 不能传count参数,保证了Counter的独立性
  }
}

场景 2:观测对象的整体替换

当使用 @ObservedV2 和 @Trace 时,对象的属性修改能被观测,但整体替换可能不刷新,这时候 @Local 就派上用场了:

@ObservedV2
class User {
  @Trace name: string;
  constructor(name: string) {
    this.name = name;
  }
}

@Entry
@ComponentV2
struct ObjectReplaceDemo {
  // 普通对象,整体替换不会触发刷新
  user: User = new User("Tom");
  
  // @Local对象,整体替换会触发刷新
  @Local localUser: User = new User("Tom");

  build() {
    Column() {
      Text(`普通user: ${this.user.name}`) // 替换对象时不刷新
      Text(`Local user: ${this.localUser.name}`) // 替换对象时会刷新
      
      Button("替换对象")
        .onClick(() => {
          this.user = new User("Jerry"); // 普通对象替换,UI不刷新
          this.localUser = new User("Jerry"); // @Local对象替换,UI刷新
        })
    }
  }
}

点击按钮后,第二个 Text 会更新,第一个不会,因为 @Local 能捕捉到对象的整体替换。

这些坑要注意:@Local 的限制和常见问题

使用 @Local 时,有些坑容易踩,提前了解能少走弯路。

问题 1:复杂类型重复赋值相同值会触发刷新

比如把同一个数组赋值给 @Local 变量,明明值一样,却会触发刷新:

@Entry
@ComponentV2
struct RepeatAssignDemo {
  list: string[][] = [['a'], ['b']];
  @Local data: string[] = this.list[0]; // 初始值是list[0]
  
  // 监控data变化
  @Monitor("data")
  onDataChange() {
    console.log("data被刷新了"); // 不该触发时也会触发
  }

  build() {
    Button("赋值相同数组")
      .onClick(() => {
        // 赋值和当前值相同的数组,依然会触发刷新
        this.data = this.list[0]; 
      })
  }
}

原因:@Local 会给数组、Map 等加代理(Proxy),代理对象和原始数组虽然内容一样,但类型不同,被判定为值变化。

解决方法:用 UIUtils.getTarget () 获取原始对象再比较:

import { UIUtils } from '@ohos.arkui.StateManagement';

@Entry
@ComponentV2
struct FixRepeatAssignDemo {
  list: string[][] = [['a'], ['b']];
  @Local data: string[] = this.list[0];
  
  @Monitor("data")
  onDataChange() {
    console.log("data真的变了");
  }

  build() {
    Button("安全赋值")
      .onClick(() => {
        // 先比较原始对象,不同再赋值
        if (UIUtils.getTarget(this.data) !== this.list[0]) {
          this.data = this.list[0];
        }
      })
  }
}

问题 2:animateTo 动画效果异常

在 V2 状态管理中,animateTo 可能不按预期工作:

@Entry
@ComponentV2
struct AnimateDemo {
  @Local w: number = 50;
  @Local h: number = 50;
  
  build() {
    Column() {
      Button("动画")
        .onClick(() => {
          // 动画前的修改可能不生效
          this.w = 100; 
          this.h = 100;
          
          this.getUIContext().animateTo({ duration: 1000 }, () => {
            this.w = 200; // 实际从50→200,不是100→200
            this.h = 200;
          })
        })
      
      Column().width(this.w).height(this.h).backgroundColor("green")
    }
  }
}

原因:V2 的刷新机制和 animateTo 不兼容,动画前的修改没被及时刷新。

解决方法:用 duration 为 0 的 animateToImmediately 先刷新:

@Entry
@ComponentV2
struct FixedAnimateDemo {
  @Local w: number = 50;
  @Local h: number = 50;
  
  build() {
    Column() {
      Button("修复动画")
        .onClick(() => {
          this.w = 100;
          this.h = 100;
          
          // 先刷新动画前的修改
          animateToImmediately({ duration: 0 }, () => {});
          
          this.getUIContext().animateTo({ duration: 1000 }, () => {
            this.w = 200; // 现在会从100→200
            this.h = 200;
          })
        })
      
      Column().width(this.w).height(this.h).backgroundColor("green")
    }
  }
}

总结一下

@Local 装饰器是 HarmonyOS 状态管理 V2 版本的重要更新,专为组件内部状态设计,解决了 @State 可能被外部修改的问题。它的核心特点是:

  1. 只在 @ComponentV2 中生效,必须本地初始化
  2. 对不同类型的观测能力不同,复杂类型需要配合 @ObservedV2 和 @Trace
  3. 内置类型(Date/Map/Set)的 API 调用可直接观测
  4. 相比 @State,状态更纯粹,适合完全内部管理的场景

掌握 @Local 的用法,能让你的组件状态更可控,UI 刷新更精准。记住那些观测规则和坑点,写代码时能少走很多弯路。下次写自定义组件时,不妨试试 @Local,体验一下更纯粹的内部状态管理吧!

Logo

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

更多推荐