鸿蒙新特性:状态管理V2 — @ObservedV2 与 @Trace 深度解析
HarmonyOS NEXT 引入 ArkUI 状态管理 V2 升级,通过 @ObservedV2、@Trace 等装饰器实现属性级细粒度响应式。相比 V1 的对象级观察,V2 能精准更新仅依赖变更属性的 UI 组件,避免无效重建。关键改进包括:1) @Trace 标记需追踪属性;2) @Computed 自动缓存计算结果;3) @Monitor 支持精确属性监听。以任务看板为例,勾选任务完成状态
HarmonyOS NEXT 对 ArkUI 状态管理系统进行了重大升级。V2 引入了
@ObservedV2、@Trace、@Computed、@Monitor等新装饰器,实现了属性级别的细粒度响应式——修改对象的一个布尔属性不再重建整个列表,只更新绑定该属性的那个 Text。本文用任务看板 Demo 展示 V2 的核心理念,并系统对比 V1 与 V2 的差异。
一、V1 的痛点:为什么需要 V2?
V1 的状态管理装饰器是很多 ArkUI 新手的第一个"坑":
@State只能做浅比较——对象变了引用才更新,对象内部属性变了不更新@Observed+@ObjectLink做类级别观察——对象的任何一个属性变化,所有绑定该对象的 UI 都重建@Prop是单向同步,父组件改了子组件跟着改,但子组件改了父组件不知道@Link双向绑定,但必须用$语法,容易写错@Provide+@Consume跨层级传递,但调试困难
最典型的问题:
// V1: 修改 task.completed,整个 TaskRow 重建
@Observed
class Task {
title: string;
completed: boolean;
}
@Component
struct TaskRow {
@ObjectLink task: Task;
build() {
Row() {
Text(this.task.title) // ← 即使 title 没变也重建
Checkbox({ select: this.task.completed })
}
}
}
一个任务有标题、完成状态、优先级、创建时间等 5 个属性。用户勾选"完成"——只改了一个 boolean——整个 Row 重建了。这个 Row 里可能还有复杂的图标、颜色计算、格式化文本,全部重新执行一遍。
V2 的解决方案:属性级追踪。
// V2: 只有绑定 completed 的 Checkbox 重建,title 不受影响
@ObservedV2
class Task {
@Trace title: string;
@Trace completed: boolean;
}
@Trace 把观察粒度从"整个对象"缩小到"单个属性"。ArkUI 框架在编译期分析每个 UI 节点的属性依赖,运行时只更新依赖了变化的那个节点的那个属性。
二、V2 装饰器全景
| 装饰器 | 作用 | 替代 V1 的 |
|---|---|---|
@ObservedV2 |
标记可观察类 | @Observed |
@Trace |
标记需追踪的属性 | (V1 无等价物) |
@ComponentV2 |
标记V2组件 | @Component |
@Local |
组件内部状态 | @State |
@Param |
接收父组件参数 | @Prop |
@Event |
子→父事件回调 | 函数回调 |
@Computed |
计算属性(自动缓存) | getter(无缓存) |
@Monitor |
属性变化监听器 | @Watch |
最大的变化不在装饰器数量,而在于思想转变:V1 是组件级响应,V2 是属性级响应。
三、@ObservedV2 + @Trace 的核心用法
定义可观察类
@ObservedV2
class TaskItem {
@Trace id: number;
@Trace title: string;
@Trace completed: boolean;
@Trace priority: number; // 1=低, 2=中, 3=高
constructor(id: number, title: string, completed: boolean, priority: number) {
this.id = id;
this.title = title;
this.completed = completed;
this.priority = priority;
}
}
每一个 @Trace 属性都是一个独立的可观察通道。修改 completed 只会通知依赖了 completed 的 UI 节点更新。
V2 组件的写法
@ComponentV2
struct TaskRow {
@Param task: TaskItem; // 接收数据(等价于 @ObjectLink)
@Event onToggle: (id: number) => void; // 事件回调
@Event onDelete: (id: number) => void;
build() {
Row() {
Checkbox({ select: this.task.completed })
.onChange(() => { this.onToggle(this.task.id); })
Text(this.task.title)
.fontColor(this.task.completed ? '#999' : '#333')
.decoration(this.task.completed ?
{ type: TextDecorationType.LineThrough } :
{ type: TextDecorationType.None })
Text('×')
.onClick(() => { this.onDelete(this.task.id); })
}
}
}
TaskRow 是 @ComponentV2 组件。当 task.completed 变化时,ArkUI 分析 build() 中哪些属性依赖了 completed:
Checkbox({ select: this.task.completed })— 依赖completed→ 更新this.task.completed ? '#999' : '#333'— 依赖completed→ 更新this.task.title— 不依赖completed→ 跳过×按钮 — 不依赖completed→ 跳过
结果是只更新了 Checkbox 的选中态和 Text 的颜色/删除线,标题文本的 Text 组件完全没有重建。

四、@Local:V2 的组件内部状态
@Local 是 @State 的 V2 等价物,但语义更清晰:
@ComponentV2
struct AddTaskBar {
@Local newTitle: string = ''; // 组件内部状态
@Event onAdd: (title: string) => void;
build() {
Row() {
TextInput({ placeholder: '输入新任务…', text: $$this.newTitle })
Button('添加')
.onClick(() => {
if (this.newTitle.trim().length > 0) {
this.onAdd(this.newTitle.trim());
this.newTitle = '';
}
})
}
}
}
和 @State 的区别在于:@Local 不能被子组件引用(语义上就是"本地状态"),而 @State 可以被 @Link 绑定。这个限制让数据流更可预测。
五、@Computed:自动缓存的计算属性
V1 中,任何计算逻辑都放在 getter 中,每次访问都重新计算:
// V1: 每次访问都重新计算
get activeCount(): number {
return this.tasks.filter(t => !t.completed).length;
}
V2 的 @Computed 自动缓存计算结果,只有当依赖的 @Trace 属性变化时才重新计算:
// V2: 自动缓存,依赖变化才重算
@Computed
get activeCount(): number {
return this.tasks.filter(t => !t.completed).length;
}
缓存失效的条件:@Computed 函数内部访问的任何一个 @Trace 属性变化 → 标记为脏 → 下次访问时重算。否则返回缓存值。
这在列表场景中效果显著——一个包含 100 个任务的列表,每次添加/删除/完成都需要重新计算统计数字。V1 每次都遍历 100 个元素,V2 只在真正变化时遍历一次。
六、@Monitor:属性变化监听
@Monitor 监听一个或多个 @Trace 属性的变化,执行副作用:
@ObservedV2
class TaskList {
@Trace tasks: TaskItem[] = [];
@Monitor('tasks')
onTasksChanged(): void {
console.log(`任务数量变为: ${this.tasks.length}`);
// 可以在这里做自动保存、同步到云端等副作用
}
@Monitor('tasks.length')
onCountChanged(): void {
// 只在 tasks 数组长度变化时触发
}
}
比 V1 的 @Watch 更精确:@Watch 只能监听 @State 变量的变化,而且是"值变了就触发",不区分是哪个子属性。@Monitor 可以指定监听的 @Trace 属性路径。
七、Demo:任务看板的 V1 实现
本 Demo 使用 V1 API(@Observed + @State + @Component)构建了一个任务看板,展示了 V2 所要解决的核心场景:
数据结构
@Observed
class TaskItem {
id: number;
title: string;
completed: boolean;
}
class TaskStats {
total: number;
active: number;
done: number;
}

统计计算
统计三项指标:总计、进行中、已完成。这些在 V2 中应该用 @Computed,这里用 getter 模拟:
private getStats(): TaskStats {
const total = this.tasks.length;
const done = this.tasks.filter(t => t.completed).length;
const stats = new TaskStats();
stats.total = total;
stats.active = total - done;
stats.done = done;
return stats;
}
筛选逻辑
private getFilteredTasks(): TaskItem[] {
if (this.filter === 'active') {
return this.tasks.filter(t => !t.completed);
}
if (this.filter === 'done') {
return this.tasks.filter(t => t.completed);
}
return this.tasks;
}
三大操作
添加任务 — 创建新实例 + 插入数组头部 + 清空输入框。[...] 展开语法确保 @State 检测到数组引用变化:
private addTask(): void {
const title = this.newTitle.trim();
if (title.length === 0) return;
this.tasks = [new TaskItem(this.nextId++, title, false), ...this.tasks];
this.newTitle = '';
}
切换完成态 — V1 的限制:必须创建新的 TaskItem 实例和新的数组来触发更新:
private toggleTask(id: number): void {
const updated: TaskItem[] = [];
for (let i = 0; i < this.tasks.length; i++) {
const t = this.tasks[i];
if (t.id === id)
updated.push(new TaskItem(t.id, t.title, !t.completed));
else
updated.push(t);
}
this.tasks = updated;
}
这种"全量重建数组"的模式是 V1 最大的性能瓶颈。在 V2 中,只需要修改 task.completed = !task.completed,框架自动精确更新。
删除任务 — 使用 filter 过滤:
private deleteTask(id: number): void {
this.tasks = this.tasks.filter(t => t.id !== id);
}
任务列表的 UI
ForEach(this.getFilteredTasks(), (task: TaskItem) => {
ListItem() {
Row() {
// 自定义圆形 Checkbox
Row()
.width(22).height(22).borderRadius(11)
.border({ width: 2, color: task.completed ?
AppColors.PRIMARY : AppColors.TEXT_DISABLED })
.backgroundColor(task.completed ?
AppColors.PRIMARY : Color.Transparent)
.onClick(() => { this.toggleTask(task.id); })
// 标题 —— 完成时变灰 + 删除线
Text(task.title)
.fontColor(task.completed ?
AppColors.TEXT_DISABLED : AppColors.TEXT_PRIMARY)
.decoration(task.completed ?
{ type: TextDecorationType.LineThrough } :
{ type: TextDecorationType.None })
// 删除按钮
Text('×')
.onClick(() => { this.deleteTask(task.id); })
}
}
}, (task: TaskItem) => task.id.toString())
筛选标签
三个标签(全部/进行中/已完成),选中项蓝色填充 + 白色文字:
@Builder
filterTab(label: string, key: string) {
Text(label)
.fontColor(this.filter === key ? Color.White : AppColors.TEXT_SECONDARY)
.backgroundColor(this.filter === key ? AppColors.PRIMARY : AppColors.BACKGROUND)
.borderRadius(BorderRadius.SM)
.onClick(() => { this.filter = key; })
}
八、V1 → V2 迁移对照表
| 场景 | V1 写法 | V2 写法 | 改进 |
|---|---|---|---|
| 可观察类 | @Observed class Task {} |
@ObservedV2 class Task { @Trace title } |
类 → 属性级 |
| 组件声明 | @Component struct X {} |
@ComponentV2 struct X {} |
支持属性级响应 |
| 组件内状态 | @State items: Item[] |
@Local items: Item[] |
语义更清晰 |
| 接收参数 | @ObjectLink item: Item |
@Param item: Item |
无需 $ 传递 |
| 子→父通信 | 函数回调参数 | @Event onXxx: () => void |
显式声明 |
| 计算属性 | getter(每次重算) | @Computed get stats() |
自动缓存 |
| 属性监听 | @Watch('data') fn() |
@Monitor('prop') fn() |
精确到属性 |
| 修改数组元素 | 创建新数组 [...arr] |
直接修改 arr[i].prop = x |
更自然 |
九、完整 Demo 代码
import { AppColors, BorderRadius, FontSize, Spacing } from '../common/Constants';
@Observed
class TaskItem {
id: number;
title: string;
completed: boolean;
constructor(id: number, title: string, completed: boolean) {
this.id = id;
this.title = title;
this.completed = completed;
}
}
class TaskStats {
total: number = 0;
active: number = 0;
done: number = 0;
}
@Entry
@Component
struct StateV2Page {
@State tasks: TaskItem[] = [];
@State newTitle: string = '';
@State filter: string = 'all';
private nextId: number = 1;
private getStats(): TaskStats {
const total = this.tasks.length;
const done = this.tasks.filter(t => t.completed).length;
const stats = new TaskStats();
stats.total = total;
stats.active = total - done;
stats.done = done;
return stats;
}
private getFilteredTasks(): TaskItem[] {
if (this.filter === 'active')
return this.tasks.filter(t => !t.completed);
if (this.filter === 'done')
return this.tasks.filter(t => t.completed);
return this.tasks;
}
private addTask(): void {
const title = this.newTitle.trim();
if (title.length === 0) return;
this.tasks = [new TaskItem(this.nextId++, title, false), ...this.tasks];
this.newTitle = '';
}
private toggleTask(id: number): void {
const updated: TaskItem[] = [];
for (let i = 0; i < this.tasks.length; i++) {
const t = this.tasks[i];
if (t.id === id)
updated.push(new TaskItem(t.id, t.title, !t.completed));
else
updated.push(t);
}
this.tasks = updated;
}
private deleteTask(id: number): void {
this.tasks = this.tasks.filter(t => t.id !== id);
}
build() {
Column() {
// Header
Row() {
Text('状态管理V2')
.fontSize(FontSize.TITLE)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
.layoutWeight(1)
.textAlign(TextAlign.Center)
}
.width('100%').height(52)
.backgroundColor(AppColors.PRIMARY)
.padding({ left: Spacing.LG, right: Spacing.LG })
// Stats bar
Row() {
this.statCard('总计', this.getStats().total, AppColors.PRIMARY)
this.statCard('进行中', this.getStats().active, '#FAAD14')
this.statCard('已完成', this.getStats().done, '#52C41A')
}
.width('100%')
.padding({ left: Spacing.LG, right: Spacing.LG, top: Spacing.MD, bottom: Spacing.MD })
.backgroundColor(Color.White)
.margin({ bottom: Spacing.SM })
.justifyContent(FlexAlign.SpaceEvenly)
// Filter tabs
Row() {
this.filterTab('全部', 'all')
this.filterTab('进行中', 'active')
this.filterTab('已完成', 'done')
}
.width('100%')
.padding({ left: Spacing.LG, right: Spacing.LG, bottom: Spacing.MD })
.backgroundColor(Color.White)
.margin({ bottom: Spacing.SM })
// Add task
Row() {
TextInput({ placeholder: '输入新任务…', text: $$this.newTitle })
.fontSize(FontSize.BODY).layoutWeight(1)
.backgroundColor(AppColors.BACKGROUND)
.borderRadius(BorderRadius.SM).padding({ left: Spacing.MD, right: Spacing.MD }).height(40)
Button('添加')
.fontSize(FontSize.CAPTION).fontColor(Color.White)
.backgroundColor(AppColors.PRIMARY)
.borderRadius(BorderRadius.SM)
.padding({ left: Spacing.LG, right: Spacing.LG }).height(40)
.margin({ left: Spacing.SM })
.onClick(() => { this.addTask(); })
}
.width('100%')
.padding({ left: Spacing.LG, right: Spacing.LG, bottom: Spacing.MD })
.backgroundColor(Color.White).margin({ bottom: Spacing.SM })
// Task list
if (this.getFilteredTasks().length === 0) {
Column() {
Text('暂无任务')
.fontSize(FontSize.BODY).fontColor(AppColors.TEXT_DISABLED)
.margin({ top: Spacing.XXL })
}
.width('100%').layoutWeight(1)
} else {
List() {
ForEach(this.getFilteredTasks(), (task: TaskItem) => {
ListItem() {
Row() {
Row()
.width(22).height(22).borderRadius(11)
.border({ width: 2, color: task.completed ?
AppColors.PRIMARY : AppColors.TEXT_DISABLED })
.backgroundColor(task.completed ?
AppColors.PRIMARY : Color.Transparent)
.justifyContent(FlexAlign.Center)
.margin({ right: Spacing.MD })
.onClick(() => { this.toggleTask(task.id); })
Text(task.title)
.fontSize(FontSize.BODY)
.fontColor(task.completed ?
AppColors.TEXT_DISABLED : AppColors.TEXT_PRIMARY)
.decoration(task.completed ?
{ type: TextDecorationType.LineThrough } :
{ type: TextDecorationType.None })
.layoutWeight(1)
Text('×')
.fontSize(FontSize.TITLE)
.fontColor(AppColors.TEXT_DISABLED)
.padding({ left: Spacing.MD, right: Spacing.XS })
.onClick(() => { this.deleteTask(task.id); })
}
.width('100%')
.padding({ left: Spacing.LG, right: Spacing.LG,
top: Spacing.MD, bottom: Spacing.MD })
.backgroundColor(Color.White)
}
}, (task: TaskItem) => task.id.toString())
}
.width('100%').layoutWeight(1).scrollBar(BarState.Off)
}
}
.width('100%').height('100%')
.backgroundColor(AppColors.BACKGROUND)
}
@Builder statCard(label: string, value: number, color: string) {
Column() {
Text(`${value}`).fontSize(32).fontWeight(FontWeight.Bold).fontColor(color)
Text(label).fontSize(FontSize.CAPTION)
.fontColor(AppColors.TEXT_TERTIARY).margin({ top: 2 })
}
.alignItems(HorizontalAlign.Center)
}
@Builder filterTab(label: string, key: string) {
Text(label)
.fontSize(FontSize.CAPTION)
.fontColor(this.filter === key ? Color.White : AppColors.TEXT_SECONDARY)
.fontWeight(this.filter === key ? FontWeight.Medium : FontWeight.Regular)
.backgroundColor(this.filter === key ? AppColors.PRIMARY : AppColors.BACKGROUND)
.borderRadius(BorderRadius.SM)
.padding({ left: Spacing.LG, right: Spacing.LG, top: Spacing.XS, bottom: Spacing.XS })
.margin({ right: Spacing.SM })
.onClick(() => { this.filter = key; })
}
}
十、常见面试题 / 踩坑点
10.1 V2 的 @Param 和 V1 的 @ObjectLink 有什么区别?
@ObjectLink 需要父组件用 $ 传递(Child({ task: $task })),@Param 直接传值(Child({ task: this.task }))。@Param 省去了 $ 的记忆负担。
10.2 @Computed 和普通 getter 有什么区别?
普通 getter 每次访问都执行函数体。@Computed 在依赖的 @Trace 属性未变化时返回缓存值。
// 每次调用都遍历数组
get activeCount(): number {
return this.tasks.filter(t => !t.completed).length;
}
// 只有 tasks 或元素 completed 变化时才重算
@Computed
get activeCount(): number {
return this.tasks.filter(t => !t.completed).length;
}
对于大数据量、高频访问的计算属性,@Computed 的缓存收益显著。
10.3 所有 V1 项目都需要迁移到 V2 吗?
不需要立即迁移。V1 API 仍然可用(虽然部分已被标记为废弃)。但新项目建议直接使用 V2——编译期优化更充分,长期维护性更好。
10.4 @ObservedV2 类中所有属性都要加 @Trace 吗?
只有需要触发 UI 更新的属性才加 @Trace。如果一个属性是纯辅助数据(如缓存的中间计算结果),不加 @Trace 可以避免不必要的重渲染。
10.5 V2 中如何实现跨组件通信?
V2 推荐的方式是通过 @ObservedV2 类作为数据源,在父组件中 @Local 持有,通过 @Param 向下传递,通过 @Event 向上传递事件。这形成了一个单向数据流:状态在父组件,子组件通过事件请求修改。
十一、总结
HarmonyOS NEXT 的状态管理从 V1 到 V2 是一次从"能用"到"好用"的质变。核心改进可以归纳为三点:
1. 粒度:组件级 → 属性级。 @Trace 让框架知道"哪个属性被哪个 UI 节点使用"——修改 completed 只更新 Checkbox 和文字颜色,不重建标题、不重建按钮。这是 V2 最大的性能红利。
2. 语义:隐式约定 → 显式声明。 @Local 比 @State 更清晰(本地 → 本地,不是全局状态),@Param 比 @ObjectLink 更简洁(无需 $),@Event 比回调参数更规范。代码的意图一眼就能看懂。
3. 工具:填补空白。 @Computed(自动缓存)、@Monitor(精确监听)解决了 V1 中长期存在的痛点。这两个装饰器在复杂业务场景中能节省大量样板代码。
V1 的装饰器清单:@State、@Prop、@Link、@ObjectLink、@Provide、@Consume、@Watch、@Observed、@StorageLink、@StorageProp。10 个装饰器,记住容易,用对难。
V2 的装饰器清单:@ObservedV2、@Trace、@ComponentV2、@Local、@Param、@Event、@Computed、@Monitor。8 个装饰器,但每个都有明确的分工,不会出现"用 @Prop 还是 @Link"的选择困难。
对于正在学习 HarmonyOS 开发的读者:如果你的项目刚起步,建议直接用 V2。如果你的项目已经是 V1,不必焦虑——V1 是可用的,理解 V2 的改进点后,可以在关键性能路径上有选择地迁移。
更多推荐




所有评论(0)