前言上一篇我们总结了 V1 的三大局限——浅观察要整体赋值、Observed 必须 new、装饰器多易混。本篇动手把「猫猫大作战」迁到V2 状态管理——用Local替 State、Param替 Prop自深观察、Event替手写函数 prop、ObservedV2Trace替 ObservedObjectLink、Monitor替 Watch。本篇是阶段二「状态管理」的收尾篇把**Local/Param 基础迁移**、Event 跨组件触发、ObservedV2Trace 字段级深观察、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 替 State2.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.etsV2 改造后 Entry ComponentV2 // ← V2 组件装饰器 struct Index { // V2Local 替 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; // V2Provide 保留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; // V2combo 改属性也触发但整体赋值更稳 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.etsV2 改造后 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 替手写函数 propV1 子组件触发父方法要手写函数 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.etsV2 改造后 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) } }五、ObservedV2Trace 字段级深观察5.1 改造 Cat// 来源entry/src/main/ets/components/GameTypes.etsV2 改造后 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装饰器标记「本字段深观察改值触发」 / 0V2 要求 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 ObservedV2 ObservedV2Trace深观察粒度整类所有字段字段级只 Trace 的子接收ObjectLinkParam自深观察必须初始值否✅ 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.etsV2 改造后 Entry ComponentV2 struct Index { Local gameState: GameState GameState.IDLE; /* ... 其他 Local */ // V2Monitor 替 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 装饰器迁移对照V1V2改动量ComponentComponentV2低改名StateLocal低改名PropParam低改名自深观察LinkParamEvent中拆双向ObservedObjectLinkObservedV2TraceParam中字段标 TraceWatchMonitor中改签名Provide/ConsumeProvide/Consume不变ReusableReusable不变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 改父 stateLink 双向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; } })九、调试技巧console.info在 Monitor 回调logmonitor.value()追触发和取值。改属性不刷新排查检查类是否 ObservedV2检查字段是否 Trace检查子是否 Param。编译报「V1 V2 混用」排查检查组件装饰器是 Component 还是 ComponentV2检查所有状态装饰器版本一致。Event 触发无响应排查检查父是否传了对应函数检查函数内是否真的改了 Local。十、性能与最佳实践整个组件统一 V1 或 V2——ComponentV2 配 Local/Param/Event/Monitor不能混。Local 自深观察——嵌套对象/数组改属性直接触发省整体赋值。Param 替 ObjectLink——接 ObservedV2 实例自深观察不用单独装饰器。Trace 字段级深观察——只标需要响应的字段省无谓追踪开销。Event 明确事件回调——比 V1 手写函数 prop 语义清晰。Monitor 多字段监听——一次监多字段嵌套字段也直接监。Trace 字段要初始值——V2 要求不赋会编译报错。Provide/Consume/Reusable 保留——V1 V2 都支持迁移时这两个不动。十一、阶段二收尾总结31–50本篇是阶段二「状态管理 交互 动画」第 20 篇阶段二收尾。回顾阶段二完整覆盖篇主题核心要点31-36定时器与主循环State、setInterval、clearTimers37-38事件与回调onClick、箭头函数 this39Watch状态变化副作用40-42跨组件数据流Prop、Link、Provide/Consume43-44深观察ObservedObjectLink、数组项替换45-46批量与嵌套batchUpdate、嵌套陷阱47-48长列表Reusable、LazyForEach49-50V1 V2 迁移V1 局限总结、V2 迁移实战阶段二核心收获V1 状态管理全套State/Prop/Link/Provide/Consume/ObservedObjectLink/Watch/Reusable/LazyForEach。定时器驱动游戏setInterval 三个周期 clearTimers 防泄漏。浅观察的坑嵌套对象/数组要整体赋值V2 Local/Param 解决。V2 迁移Local/Param/Event/ObservedV2Trace/Monitor深观察省整体赋值。长列表优化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明确事件、ObservedV2Trace 字段级深观察**、Monitor 替 Watch多字段监听五大要点并给出了猫猫大作战 V2 迁移完整代码。核心要点ComponentV2 统一 V2Local/Param 自深观察省整体赋值Trace 字段级精细Event 明确事件Monitor 多字段监听。下一篇我们将进入阶段三拆解 onTouch——按下/移动/抬起全手势。如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力相关资源「猫猫大作战」项目源码本仓库entry/src/main/ets/pages/Index.ets、entry/src/main/ets/components/ArkUI V2 状态管理官方指南ArkUI Local/Param 官方文档ArkUI ObservedV2Trace 深观察官方指南ArkUI Monitor 监听官方文档开源鸿蒙跨平台社区HarmonyOS 开发者官方文档首页系列索引本仓库articles/INDEX.md