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 的改进点后,可以在关键性能路径上有选择地迁移。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐