@Local vs @Param vs @Trace 组件状态管理三剑客
一、引言
在 HarmonyOS V2 装饰器体系中,@Local、@Param 和 @Trace 是三个最容易混淆的状态装饰器。它们各自服务于不同的场景------组件内部状态、父组件传入参数、跨组件共享数据。理解这三者的区别和组合使用方式,是构建健壮的 HarmonyOS 应用的基础。
本文将以一个英语学习 App 中的 CourseHomePage(单词卡片学习页)为案例,深入解析这三种装饰器的职责边界和最佳实践。

二、@Local:组件内部可变状态
2.1 概念与定位
@Local 是 V2 装饰器体系中管理组件内部可变状态 的装饰器。它替代了 V1 中的 @State 和 @Local,用于声明那些只属于当前组件、不被父组件直接控制的状态变量。
2.2 CourseHomePage 中的 @Local 实战
在单词卡片学习页面中,我们使用 @Local 管理 8 个 UI 状态字段:
typescript
// CourseHomePage.ets
@ComponentV2
export struct CourseHomePage {
@Local currentIndex: number = 0; // 当前卡片索引
@Local isFlipped: boolean = false; // 卡片是否翻转
@Local currentCategory: string = 'all'; // 当前分类
@Local learnedCount: number = 0; // 已学单词数量
@Local masterCount: number = 0; // 已掌握数量
@Local rotateY: number = 0; // 翻转 Y 轴角度
@Local frontOpacity: number = 1; // 卡片正面透明度
@Local backOpacity: number = 0; // 卡片背面透明度
@Local isFavorite: boolean = false; // 收藏状态
}
这些 @Local 变量的作用:
| 变量 | 类型 | 作用域 | 说明 |
|---|---|---|---|
currentIndex |
number |
当前页面 | 跟踪当前显示的是第几个单词 |
isFlipped |
boolean |
当前页面 | 控制卡片的翻转状态 |
currentCategory |
string |
当前页面 | 当前选中的单词分类 |
learnedCount |
number |
当前页面 | 统计已学习的单词数 |
rotateY/frontOpacity/backOpacity |
number |
当前页面 | 控制翻转动画的参数 |
isFavorite |
boolean |
当前页面 | 标记当前单词是否收藏 |
2.3 @Local 的特性
typescript
import { AppStorageV2 } from '@kit.ArkUI';
@ComponentV2
export struct HomePage {
// ✅ 简单类型
@Local count: number = 0;
@Local name: string = '';
// ✅ 复杂类型(数组、对象)
@Local functions: CourseCoreBar[] = [/* ... */];
@Local coursePractice: CourseQuestions[] = [/* ... */];
// ✅ 与 AppStorageV2 连接获取全局状态
@Local prop: Sample = AppStorageV2.connect(Sample, () => new Sample())!;
@Local logoUser: UserInfo = AppStorageV2.connect(UserInfo, () => new UserInfo())!;
}
关键特性:
- 组件私有:@Local 变量被其他组件访问
- 可变性:组件内部可以随意修改 @Local 变量
- 自动更新:修改 @Local 变量时,模板中引用该变量的位置自动重渲染
- 初始化必须赋值:@Local 变量必须有初始值
2.4 @Local 的典型使用场景
- UI 交互状态:翻转、展开、选中等状态
- 表单输入:输入框的值、校验状态
- 临时计算结果:过滤后的列表、统计值
- 组件内路由:当前 Tab 索引、页面内部导航
三、@Param:父组件传入的外部参数
3.1 概念与定位
@Param 是 V2 装饰器体系中用于接收父组件传入参数 的装饰器,替代了 V1 中的 @Prop 和一部分 @Link 的职责。
3.2 @Param 的典型用法
typescript
// SubComponent.ets(模拟示例)
@ComponentV2
export struct WordCardItem {
@Param word: WordCard = new WordCard(0, '', '', '', '', '', '');
@Param onFavoriteChange?: (id: number, isFavorite: boolean) => void;
build() {
Column() {
Text(this.word.word) // 使用父组件传入的数据
.fontSize(20);
Button('收藏')
.onClick(() => {
this.onFavoriteChange?.(this.word.id, true); // 通过回调通知父组件
});
}
}
}
3.3 @Param 的特性
- 只读约束:子组件不能修改 @Param 的值(修改会触发编译警告)
- 父组件驱动:父组件重新渲染时,会传入新的参数值
- 可选的:@Param 可以没有默认值,表示可选参数
- 配合回调实现双向通信 :通过
@Param + 回调函数模式实现子组件到父组件的通信
3.4 项目中 @Param 的应用
在 MainPage.ets 中,ReusableFlowItem 组件通过 @Param 接收数据:
typescript
@ComponentV2
struct ReusableFlowItem {
@Param item: PracticeView = new PracticeView($r('app.media.ic_home'), '顺序练习', '1、3256');
build() {
Row() {
Text(this.item.name) // 使用父组件传入的数据
Text(this.item.describe) // 使用父组件传入的数据
}
}
}
父组件在 Grid 中循环创建子组件时传递参数:
typescript
LazyForEach(this.dataSource, (practiceItem: PracticeView) => {
GridItem() {
ReusableFlowItem({ item: practiceItem }); // 传入 @Param
}
});
四、@Trace:跨组件共享的响应式数据
4.1 概念与定位
@Trace 是类字段级别的响应式装饰器 ,配合 @ObservedV2 使用。它让类的字段变得可观察,任何组件只要引用了该字段,就能自动追踪其变化。
4.2 @Trace 的跨组件共享
typescript
// WordCardModel.ets - 被多个组件共享的数据模型
@ObservedV2
export class WordCard {
@Trace id: number = 0;
@Trace word: string = '';
@Trace phonetic: string = '';
@Trace translation: string = '';
@Trace example: string = '';
// ...
}
// CourseHomePage.ets - 消费端
@Entry
@ComponentV2
export struct CourseHomePage {
// 获取当前单词
private currentWord(): WordCard {
const words = this.getFilteredWords();
return words[this.currentIndex];
}
build() {
Text(this.currentWord().word) // 追踪 @Trace word
Text(this.currentWord().phonetic) // 追踪 @Trace phonetic
Text(this.currentWord().translation) // 追踪 @Trace translation
}
}
4.3 @Trace 与 @Local/@Param 的本质区别
| 维度 | @Local | @Param | @Trace |
|---|---|---|---|
| 定义位置 | 组件内 | 组件内 | 类定义中 |
| 数据源 | 组件自身 | 父组件 | 数据模型实例 |
| 可写性 | 组件内可写 | 只读 | 通过实例引用可写 |
| 作用域 | 单组件 | 父子组件 | 任意引用该实例的组件 |
| 更新方式 | this.xxx = value | 父组件重新传入 | instance.xxx = value |
五、三者组合使用的模式
5.1 典型组合模式
在实际开发中,三者的组合通常遵循"状态就近原则":
typescript
@ComponentV2
export struct CourseHomePage {
// @Local:组件内部状态
@Local currentIndex: number = 0;
@Local isFlipped: boolean = false;
// @Trace 实例的组合使用
private currentWord(): WordCard {
return DEFAULT_WORD_CARDS[this.currentIndex];
}
// 数据源
private dataSource: WordCardDataSource = new WordCardDataSource(DEFAULT_WORD_CARDS);
build() {
Column() {
// 显示 @Trace 字段的值
Text(this.currentWord().word)
.fontSize(34)
.fontWeight(FontWeight.Bold);
// 修改 @Trace 字段
Button('已掌握')
.onClick(() => {
this.learnedCount++; // 修改 @Local
this.nextCard(); // 修改 @Local
});
}
}
}
5.2 状态就近原则
"数据在哪用,就在哪定义" 是状态管理的核心原则:
- 只在当前组件内使用的状态 →
@Local - 需要父组件传入的数据 →
@Param - 需要跨组件共享的业务数据 →
@ObservedV2+@Trace - 全局唯一、跨模块共享的状态 →
AppStorageV2+@Local
5.3 实践中避免的错误
typescript
// ❌ 错误:把本应是 @Param 的数据用 @Local 管理
@ComponentV2
export struct BadComponent {
@Local word: WordCard = new WordCard(0, '', '', '', '', '', '');
// 父组件无法控制这个组件的 word 数据
}
// ✅ 正确:父组件传入的数据用 @Param
@ComponentV2
export struct GoodComponent {
@Param word: WordCard = new WordCard(0, '', '', '', '', '', '');
}
六、性能考量
6.1 @Local 的更新范围
@Local 变量变化时,整个组件的 build 方法会被重新执行。因此,如果一个组件有多个 @Local 变量且 build 逻辑复杂,建议拆分为子组件。
6.2 @Trace 的细粒度追踪
@Trace 的优势在于其细粒度追踪。当 WordCard.word 变化时,只有引用了 .word 的组件会更新,不会影响到引用 .phonetic 的组件。这种机制在大数据量场景下优势明显。
6.3 状态存储位置的选择路径
数据是否需要跨组件共享?
├── 否 → @Local(组件内使用)
└── 是 → 数据是否由父组件传入?
├── 是 → @Param(父子通信)
└── 否 → 数据是否需要全局唯一?
├── 是 → AppStorageV2 + @Local
└── 否 → @ObservedV2 + @Trace
七、最佳实践总结
- @Local 用于 UI 交互状态:翻转、选中、输入等组件内部状态
- @Param 用于父组件传参:配合回调函数实现子到父的通信
- @Trace 用于跨组件共享:定义在数据模型中,被多个组件消费
- 状态就近定义:不需要共享的状态不要提升到公共位置
- 合理拆分组件:一个组件的 @Local 变量超过 10 个时,考虑拆分
- 避免 @Param 多层透传:超过 3 层时考虑状态提升或 AppStorageV2
八、总结
@Local、@Param、@Trace 分别解决了组件内部状态、父子传参、跨组件共享三种场景的需求。理解这三者的定位和边界,选择合适的装饰器管理状态,是 HarmonyOS V2 声明式开发的基础能力。在实践中坚持"状态就近原则",可以让组件的状态管理更加清晰和高效。