文章配图:@Local/@Param 基础迁移、@Event 跨组件触发、@ObservedV2+@Trace 字段级

页面预览

前言

上一篇我们总结了 V1 的三大局限——浅观察要整体赋值、@Observed 必须 new、装饰器多易混。本篇动手把「猫猫大作战」迁到 V2 状态管理——用 @Local 替 @State、@Param 替 @Prop(自深观察)、@Event 替手写函数 prop、@ObservedV2+@Trace 替 @Observed+@ObjectLink、@Monitor 替 @Watch。

本篇是阶段二「状态管理」的收尾篇,把**@Local/@Param 基础迁移**、@Event 跨组件触发@ObservedV2+@Trace 字段级深观察@Monitor 多字段监听四大要点讲透。

提示:本系列不讲 ArkTS 基础语法与环境搭建,假设你已跟完第 1–49 篇。本篇是阶段二第 20 篇,V2 迁移实战。

一、场景拆解:迁 V2 的改造点

回顾「猫猫大作战」当前 V1 状态(第 49 篇总结):

// V1 版
@Entry
@Component
struct Index {
  @State @Watch('onGameStateChange') gameState: GameState = GameState.IDLE;
  @State score: number = 0;
  @State combo: ComboInfo = { count: 0, multiplier: 1, lastMergeTime: 0 };
  @State cats: Cat[] = [];
  /* ... */
}

@Observed
export class Cat { /* 整类深观察 */ }

@Component
export struct CatItem {
  @ObjectLink cat: Cat;       // 必须用 @ObjectLink
  build() { /* ... */ }
}

@Component
export struct PauseOverlay {
  @Link gameState: GameState;     // 双向,$val 传
  @Prop score: number;
  build() { /* ... */ }
}

V2 迁移后

// V2 版
@Entry
@ComponentV2
struct Index {
  @Local gameState: GameState = GameState.IDLE;
  @Local score: number = 0;
  @Local combo: ComboInfo = { count: 0, multiplier: 1, lastMergeTime: 0 };
  @Local cats: Cat[] = [];
  /* ... */

  @Monitor('gameState')
  onGameStateChange(monitor: Monitor): void { /* ... */ }
}

@ObservedV2
export class Cat {
  @Trace id: string = '';
  @Trace level: CatLevel = 0;
  @Trace x: number = 0;
  @Trace y: number = 0;          // 字段级深观察
  @Trace falling: boolean = false;
}

@ComponentV2
export struct CatItem {
  @Param cat: Cat;               // @Param 自深观察,不用 @ObjectLink
  build() { /* ... */ }
}

@ComponentV2
export struct PauseOverlay {
  @Param gameState: GameState;   // V2 @Param 改了也同步(但要 @Event 回传改)
  @Param score: number;
  @Event onResume: () => void;    // 子触发父方法
  build() { /* ... */ }
}

关键经验V2 用 @ComponentV2 替代 @Component——整个组件 V2 化,装饰器全换 V2 版。

二、@Local 替 @State

2.1 基本迁移

// V1
@State score: number = 0;

// V2
@Local score: number = 0;
``

**拆解**| 片段 | 含义 |
|------|------|
| `@Local` | 装饰器,标记「本地响应式状态」 |
| `score` | 变量名 |
| `number` | 类型 |
| `= 0` | 初始值 |

**语义差异**@State 是「状态」,@Local 强调「本地」(组件私有),更清晰。

### 2.2 改属性直接触发(深观察)

```ts
// V2 @Local 对象改属性直接触发
@Local combo: ComboInfo = { count: 0, multiplier: 1, lastMergeTime: 0 };

this.combo.count = 5;       // ✅ V2 直接触发!
// V1 要 this.combo = { ...this.combo, count: 5 };

关键经验V2 @Local 自深观察——改对象/数组内部属性直接触发,省整体赋值。这是 V2 最大收益。

2.3 完整迁移 Index 的 @State

// 来源:entry/src/main/ets/pages/Index.ets(V2 改造后)
@Entry
@ComponentV2                    // ← V2 组件装饰器
struct Index {
  // V2:@Local 替 @State
  @Local gameState: GameState = GameState.IDLE;
  @Local score: number = 0;
  @Local cats: Cat[] = [];
  @Local combo: ComboInfo = { count: 0, multiplier: 1, lastMergeTime: 0 };
  @Local nextCatLevel: CatLevel = CatLevel.SMALL;
  @Local highScore: number = 0;
  @Local gameTime: number = 0;
  @Local maxCombo: number = 0;
  @Local mergeCount: number = 0;
  @Local highestLevel: CatLevel = CatLevel.SMALL;

  // V2:@Provide 保留(V1 V2 都支持)
  @Provide('gameState') providedGameState: GameState = this.gameState;
  @Provide('score') providedScore: number = this.score;

  private gameEngine: GameEngine = new GameEngine();
  private gameLoopTimer: number = -1;
  private spawnTimer: number = -1;
  private timeTimer: number = -1;
  private readonly cols: number[] = [0, 1, 2, 3, 4];
  private readonly rows: number[] = [0, 1, 2, 3, 4, 5, 6, 7];

  startGame() {
    this.clearTimers();
    this.gameEngine.reset();
    this.gameState = GameState.PLAYING;
    this.score = 0;
    this.cats = [];
    this.gameTime = 0;
    // V2:combo 改属性也触发,但整体赋值更稳
    this.combo = { count: 0, multiplier: 1, lastMergeTime: 0 };
    this.nextCatLevel = this.gameEngine.getNextCatLevel();

    this.gameLoopTimer = setInterval(() => {
      if (this.gameState !== GameState.PLAYING) return;
      this.cats = this.gameEngine.updateCats();
      this.score = this.gameEngine.getScore();
      this.combo = this.gameEngine.getCombo();
      if (this.gameEngine.isGameOver()) { this.endGame(); }
    }, 100);

    /* spawnTimer、timeTimer 筑略 */
  }

  /* 其他方法不变 */
}

三、@Param 替 @Prop(自深观察)

3.1 基本迁移

// V1
@Prop score: number;

// V2
@Param score: number;

语义差异:@Param 强调「参数」(从父接收),且自深观察——接 @ObservedV2 实例时改属性直接触发。

3.2 @Param 深观察实例

// V2 Cat 是 @ObservedV2
@ObservedV2
export class Cat {
  @Trace x: number = 0;
  @Trace y: number = 0;
}

// V2 子组件用 @Param 接 Cat,自深观察
@ComponentV2
export struct CatItem {
  @Param cat: Cat;          // ← @Param 替 @ObjectLink
  build() {
    Column() { /* ... */ }
      .position({ x: this.cat.x * 60, y: this.cat.y * 60 })
  }
}

// 父改 cat.y,子 CatItem 重渲染
this.cats[0].y = 7;        // ✅ V2 直接触发,不用整体赋值数组

关键经验V2 @Param 替代 @ObjectLink——@Param 接 @ObservedV2 实例时自深观察,不用单独装饰器。

3.3 改造 CatItem

// 来源:entry/src/main/ets/components/CatItem.ets(V2 改造后)
import { Cat, CatConfig } from './GameTypes';

@ComponentV2                    // ← V2
export struct CatItem {
  @Param cat: Cat;             // ← @Param 替 @ObjectLink

  build() {
    Column() {
      Text(CatConfig[this.cat.level].emoji)
        .fontSize(CatConfig[this.cat.level].size * 0.5)
    }
    .width(CatConfig[this.cat.level].size)
    .height(CatConfig[this.cat.level].size)
    .borderRadius(CatConfig[this.cat.level].size / 2)
    .backgroundColor(CatConfig[this.cat.level].color)
    .justifyContent(FlexAlign.Center)
    .shadow({ radius: 4, color: 'rgba(0,0,0,0.2)', offsetY: 2 })
    .position({
      x: this.cat.x * 60 + (60 - CatConfig[this.cat.level].size) / 2,
      y: this.cat.y * 60 + (60 - CatConfig[this.cat.level].size) / 2
    })
    .animation({ duration: 100, curve: Curve.Linear })
  }
}

四、@Event 跨组件触发

4.1 替手写函数 prop

V1 子组件触发父方法要手写函数 prop:

// V1
@Component
export struct PauseOverlay {
  onResume: () => void;        // 手写函数 prop
  build() {
    Button('继续').onClick(() => { this.onResume(); })
  }
}
// 父:PauseOverlay({ onResume: () => { this.gameState = GameState.PLAYING; } })

V2 用 @Event 装饰器:

// V2
@ComponentV2
export struct PauseOverlay {
  @Event onResume: () => void;   // ← @Event 装饰
  build() {
    Button('继续').onClick(() => { this.onResume(); })
  }
}
// 父:PauseOverlay({ onResume: () => { this.gameState = GameState.PLAYING; } })
``

**关键经验****@Event 明确标记「事件回调」**——比 V1 手写函数 prop 语义清晰,IDE 能识别。

### 4.2 @Param + @Event@Link

V1@Link 双向绑定,V2 拆成 @Param(只读显示)+ @Event(触发改):

```ts
// V1 @Link
@Component
export struct PauseOverlay {
  @Link gameState: GameState;
  build() {
    Button('继续').onClick(() => { this.gameState = GameState.PLAYING; })
  }
}
// 父:PauseOverlay({ gameState: this.$gameState })

// V2 @Param + @Event
@ComponentV2
export struct PauseOverlay {
  @Param gameState: GameState;        // 只读显示
  @Event onResume: () => void;        // 触发父改
  @Event onRestart: () => void;
  @Event onBackToMenu: () => void;
  build() {
    Button('继续').onClick(() => { this.onResume(); })
    Button('重新开始').onClick(() => { this.onRestart(); })
    Button('返回主菜单').onClick(() => { this.onBackToMenu(); })
  }
}
// 父:PauseOverlay({
//   gameState: this.gameState,
//   onResume: () => { this.gameState = GameState.PLAYING; },
//   onRestart: () => { this.clearTimers(); this.startGame(); },
//   onBackToMenu: () => { this.clearTimers(); this.gameState = GameState.IDLE; }
// })

关键经验V2 拆 @Link 为 @Param(数据)+ @Event(触发)——数据流单向只读,事件流单向触发,更清晰。

4.3 改造 PauseOverlay

// 来源:entry/src/main/ets/components/PauseOverlay.ets(V2 改造后)
import { GameState } from './GameTypes';

@ComponentV2
export struct PauseOverlay {
  @Param gameState: GameState;       // 只读显示当前状态
  @Param score: number;              // 只读显示得分
  @Event onResume: () => void;       // ← V2 @Event
  @Event onRestart: () => void;
  @Event onBackToMenu: () => void;

  build() {
    Column() {
      Column() {
        Text('⏸️').fontSize(48).margin({ bottom: 12 })
        Text('游戏暂停').fontSize(24).fontWeight(FontWeight.Bold).fontColor('#2C3E50').margin({ bottom: 8 })
        Text(`当前得分: ${this.score}`).fontSize(16).fontColor('#7F8C8D').margin({ bottom: 24 })

        Button('继续游戏')
          .width('80%').height(48)
          .fontSize(17).fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF').backgroundColor('#2ECC71')
          .borderRadius(24).margin({ bottom: 12 })
          .onClick(() => { this.onResume(); })      // ← 触发 @Event

        Button('重新开始')
          .width('80%').height(48)
          .fontSize(17)
          .fontColor('#FFFFFF').backgroundColor('#F39C12')
          .borderRadius(24).margin({ bottom: 12 })
          .onClick(() => { this.onRestart(); })

        Button('返回主菜单')
          .width('80%').height(48)
          .fontSize(17)
          .fontColor('#7F8C8D').backgroundColor('#ECF0F1')
          .borderRadius(24)
          .onClick(() => { this.onBackToMenu(); })
      }
      .width('80%').padding(32)
      .backgroundColor('#FFFFFF').borderRadius(20)
      .shadow({ radius: 20, color: 'rgba(0,0,0,0.15)', offsetY: 8 })
      .alignItems(HorizontalAlign.Center)
    }
    .width('100%').height('100%')
    .backgroundColor('rgba(44, 62, 80, 0.6)')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  }
}

五、@ObservedV2+@Trace 字段级深观察

5.1 改造 Cat

// 来源:entry/src/main/ets/components/GameTypes.ets(V2 改造后)
@ObservedV2                    // ← V2 整类标记
export class Cat {
  @Trace id: string = '';      // ← 字段级深观察
  @Trace level: CatLevel = 0;
  @Trace x: number = 0;
  @Trace y: number = 0;
  @Trace falling: boolean = false;

  constructor(id: string, level: CatLevel, x: number, y: number, falling: boolean) {
    this.id = id;
    this.level = level;
    this.x = x;
    this.y = y;
    this.falling = falling;
  }
}

拆解

片段 含义
@ObservedV2 装饰器,标记「V2 可深观察类」
@Trace 装饰器,标记「本字段深观察,改值触发」
= '' / = 0 V2 要求 @Trace 字段有初始值

关键经验V2 @Trace 字段级深观察——只标需要响应的字段,不标的字段改了不触发,省无谓追踪。

5.2 改属性直接触发

// V2 Cat 是 @ObservedV2 + @Trace y
const cat = new Cat('cat_0', CatLevel.SMALL, 0, 0, true);

cat.y = 7;        // ✅ 改 @Trace y,触发所有依赖 cat.y 的 CatItem 重渲染
cat.id = 'cat_1'; // ✅ 改 @Trace id,触发
// 不 @Trace 的字段改了不触发(如果有的话)

5.3 与 V1 @Observed 对比

维度 V1 @Observed V2 @ObservedV2+@Trace
深观察粒度 整类所有字段 字段级(只 @Trace 的)
子接收 @ObjectLink @Param(自深观察)
必须初始值 ✅ @Trace 字段要初始值
性能 中(全字段追踪) 高(只追踪 @Trace)

关键经验V2 @Trace 字段级追踪性能更优——只监听需要响应的字段,不费的不管。

六、@Monitor 替 @Watch(多字段监听)

6.1 基本迁移

// V1
@State @Watch('onGameStateChange') gameState: GameState = GameState.IDLE;

onGameStateChange(newVal: GameState): void {
  switch (newVal) { /* ... */ }
}

// V2
@Local gameState: GameState = GameState.IDLE;

@Monitor('gameState')           // ← V2 @Monitor
onGameStateChange(monitor: Monitor): void {
  const newVal = monitor.value(GameState);
  switch (newVal) { /* ... */ }
}
``

**拆解**| 片段 | 含义 |
|------|------|
| `@Monitor('gameState')` | 装饰器,监听 gameState 变化 |
| `onGameStateChange` | 回调方法名 |
| `monitor: Monitor` | 监听器对象,能取变后的值 |

### 6.2 多字段监听(V2 新能力)

```ts
// V2:一次监多字段
@Local combo: ComboInfo = { count: 0, multiplier: 1, lastMergeTime: 0 };

@Monitor('combo.count', 'combo.multiplier')    // ← 监两个嵌套字段
onComboChange(monitor: Monitor): void {
  console.info('combo 变了', monitor.value(combo));
  if (monitor.value(combo).count >= 3) {
    this.vibrate();      // 3 连击以上震动
  }
}

// V1 要两个 @Watch 分别监,V2 一个 @Monitor 搞定

关键经验V2 @Monitor 一次监多字段——比 V1 @Watch 一字段一回调更省,嵌套字段也直接监。

6.3 改造 onGameStateChange

// 来源:entry/src/main/ets/pages/Index.ets(V2 改造后)
@Entry
@ComponentV2
struct Index {
  @Local gameState: GameState = GameState.IDLE;
  /* ... 其他 @Local */

  // V2:@Monitor 替 @Watch(本篇重点)
  @Monitor('gameState')
  onGameStateChange(monitor: Monitor): void {
    const newVal = monitor.value(GameState);
    switch (newVal) {
      case GameState.PLAYING:
        this.playSfx('start');
        break;
      case GameState.PAUSED:
        this.playSfx('pause');
        break;
      case GameState.GAME_OVER:
        this.playSfx('over');
        this.vibrate();
        break;
      case GameState.IDLE:
        this.playSfx('menu');
        this.clearTimers();      // 切主菜单清定时器
        break;
    }
  }

  playSfx(name: string): void { console.info(`playSfx: ${name}`); }
  vibrate(): void { console.info('vibrate'); }

  /* ... 其他方法 */
}

七、完整 V2 迁移对比

7.1 装饰器迁移对照

V1 V2 改动量
@Component @ComponentV2 低(改名)
@State @Local 低(改名)
@Prop @Param 低(改名,自深观察)
@Link @Param + @Event 中(拆双向)
@Observed + @ObjectLink @ObservedV2 + @Trace + @Param 中(字段标 @Trace)
@Watch @Monitor 中(改签名)
@Provide/@Consume @Provide/@Consume 不变
@Reusable @Reusable 不变

7.2 核心收益

痛点 V1 解法 V2 解法
combo 嵌套对象改属性 {...this.combo, count: 5} this.combo.count = 5 直接触发
cats 数组项改属性 引擎返回新数组整体赋值 this.cats[0].y = 7 直接触发
Cat 深观察要 @ObjectLink 子组件单独装饰 @Param 自深观察
gameState 监听单字段 @Watch 一字段一回调 @Monitor 多字段一次监
PauseOverlay 改父 state @Link 双向 @Param + @Event 分离

关键经验V2 迁移核心收益是「嵌套对象/数组改属性直接触发」——省整体赋值,代码量减半。

八、踩坑提示

8.1 V1 V2 同组件混用

// ❌ 错误:同组件混用,编译报错
@ComponentV2
struct Bad {
  @State score: number = 0;        // V1
  @Local combo: ComboInfo = { /* ... */ };   // V2
}

// ✅ 正确:整个组件统一 V2
@ComponentV2
struct Good {
  @Local score: number = 0;
  @Local combo: ComboInfo = { /* ... */ };
}

8.2 @Trace 字段忘初始值

// ❌ 错误:@Trace 字段没初始值,编译报错
@ObservedV2
export class Cat {
  @Trace y: number;       // 没初始值
}

// ✅ 正确:@Trace 字段要初始值
@ObservedV2
export class Cat {
  @Trace y: number = 0;   // 有初始值
}

8.3 忘 @ComponentV2

// ❌ 错误:用了 @Local 但还是 @Component,编译报错
@Component
struct Bad {
  @Local score: number = 0;       // V2 装饰器配 V1 组件
}

// ✅ 正确:@ComponentV2
@ComponentV2
struct Good {
  @Local score: number = 0;
}

8.4 @Monitor 忘取值

// ❌ 错误:@Monitor 回调没参数,取不到新值
@Monitor('gameState')
onGameStateChange(): void {
  console.info(this.gameState);      // 可能还是旧值(时机问题)
}

// ✅ 正确:用 monitor.value 取新值
@Monitor('gameState')
onGameStateChange(monitor: Monitor): void {
  const newVal = monitor.value(GameState);   // 取新值
  console.info(newVal);
}

8.5 @Event 忘在父传函数

// ❌ 错误:子声明了 @Event 但父没传函数,运行时报错
PauseOverlay({ gameState: this.gameState })   // 忘了 onResume 等

// ✅ 正确:所有 @Event 都要在父传
PauseOverlay({
  gameState: this.gameState,
  onResume: () => { this.gameState = GameState.PLAYING; },
  onRestart: () => { this.clearTimers(); this.startGame(); },
  onBackToMenu: () => { this.clearTimers(); this.gameState = GameState.IDLE; }
})

九、调试技巧

  1. console.info 在 @Monitor 回调:log monitor.value(),追触发和取值。
  2. 改属性不刷新排查:检查类是否 @ObservedV2;检查字段是否 @Trace;检查子是否 @Param。
  3. 编译报「V1 V2 混用」排查:检查组件装饰器是 @Component 还是 @ComponentV2;检查所有状态装饰器版本一致。
  4. @Event 触发无响应排查:检查父是否传了对应函数;检查函数内是否真的改了 @Local。

十、性能与最佳实践

  1. 整个组件统一 V1 或 V2——@ComponentV2 配 @Local/@Param/@Event/@Monitor,不能混。
  2. @Local 自深观察——嵌套对象/数组改属性直接触发,省整体赋值。
  3. @Param 替 @ObjectLink——接 @ObservedV2 实例自深观察,不用单独装饰器。
  4. @Trace 字段级深观察——只标需要响应的字段,省无谓追踪开销。
  5. @Event 明确事件回调——比 V1 手写函数 prop 语义清晰。
  6. @Monitor 多字段监听——一次监多字段,嵌套字段也直接监。
  7. @Trace 字段要初始值——V2 要求,不赋会编译报错。
  8. @Provide/@Consume/@Reusable 保留——V1 V2 都支持,迁移时这两个不动。

十一、阶段二收尾总结(31–50)

本篇是阶段二「状态管理 + 交互 + 动画」第 20 篇,阶段二收尾。回顾阶段二完整覆盖:

主题 核心要点
31-36 定时器与主循环 @State、setInterval、clearTimers
37-38 事件与回调 onClick、箭头函数 this
39 @Watch 状态变化副作用
40-42 跨组件数据流 @Prop、@Link、@Provide/@Consume
43-44 深观察 @Observed+@ObjectLink、数组项替换
45-46 批量与嵌套 batchUpdate、嵌套陷阱
47-48 长列表 @Reusable、LazyForEach
49-50 V1 V2 迁移 V1 局限总结、V2 迁移实战

阶段二核心收获

  1. V1 状态管理全套:@State/@Prop/@Link/@Provide/@Consume/@Observed+@ObjectLink/@Watch/@Reusable/LazyForEach。
  2. 定时器驱动游戏:setInterval 三个周期 + clearTimers 防泄漏。
  3. 浅观察的坑:嵌套对象/数组要整体赋值,V2 @Local/@Param 解决。
  4. V2 迁移:@Local/@Param/@Event/@ObservedV2+@Trace/@Monitor,深观察省整体赋值。
  5. 长列表优化:@Reusable 复用池 + LazyForEach 按需渲染。

接下来阶段三(51–70)将进入交互与动画:onClick/onTouch/onHover/onKeyEvent 事件四件套、animateTo 显式动画、属性动画、Hero 共享元素、transition 转场、bindSheet/app bindUI ContentType 路由、spring 物理、Hero Style Player 等。

总结

本篇我们做 V2 迁移实战,掌握了**@Local 替 @State(自深观察)@Param 替 @Prop/@ObjectLink(接 @ObservedV2 自深观察)@Event 替手写函数 prop(明确事件)@ObservedV2+@Trace 字段级深观察**、@Monitor 替 @Watch(多字段监听)五大要点,并给出了猫猫大作战 V2 迁移完整代码。核心要点:@ComponentV2 统一 V2;@Local/@Param 自深观察省整体赋值;@Trace 字段级精细;@Event 明确事件;@Monitor 多字段监听

下一篇我们将进入阶段三,拆解 onTouch——按下/移动/抬起全手势。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

Logo

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

更多推荐