行业资讯
📅 2026/7/28 3:17:02
HarmonyOS应用开发实战:猫猫大作战-@Link 声明与 $val 传参、双向同步机制、@Link vs @Prop vs @Event 取舍、
前言上一篇我们用Prop把得分从父传给子 HUD——单向只读子改不了父。但有些场景子组件要直接改父的数据表单子组件改父的 username、滑块子组件改父的音量、暂停弹窗里的「继续/重开」按钮改父的 gameState。这时Prop不够用要用Link双向绑定——子改父同步父改子也同步。本篇以「猫猫大作战」暂停弹窗子组件用Link改父 gameState 为锚点把Link 声明与 $val 传参、双向同步机制、Link vs Prop vs Event 取舍、双向数据流的边界四大要点讲透。提示本系列不讲 ArkTS 基础语法与环境搭建假设你已跟完第 1–40 篇。本篇是阶段二第十一篇。一、场景拆解暂停弹窗改父 gameState回顾「猫猫大作战」PauseOverlay第 93 篇会专讲此处简提// 来源entry/src/main/ets/pages/Index.ets PauseOverlay() Builder PauseOverlay() { Column() { Text(游戏暂停) Button(继续游戏).onClick(() { this.resumeGame(); }) // ← 调父方法 Button(重新开始).onClick(() { this.clearTimers(); this.startGame(); }) Button(返回主菜单).onClick(() { this.clearTimers(); this.gameState GameState.IDLE; }) } }现在想把 PauseOverlay 拆成独立子组件PauseOverlay子组件的「继续」按钮要改父的 gameState——用LinkComponent export struct PauseOverlay { Link gameState: GameState; // 双向绑定父的 gameState Prop score: number; // 只读显示当前得分 build() { Button(继续游戏).onClick(() { this.gameState GameState.PLAYING; // ← 子直接改 Link同步到父 }) /* ... */ } } // 父 PauseOverlay({ gameState: this.$gameState, score: this.score })关键经验Link 父↔子双向引用——子改 Link 等于改父的 State父改 State 子也同步。二、Link 基本用法2.1 子组件声明 LinkComponent export struct PauseOverlay { Link gameState: GameState; // 双向绑定 Prop score: number; // 只读显示 build() { Column() { Text(游戏暂停) Text(当前得分: ${this.score}) // 读 Prop Button(继续游戏).onClick(() { this.gameState GameState.PLAYING; // 改 Link同步父 }) Button(重新开始).onClick(() { this.gameState GameState.IDLE; // 改 Link触发父切主菜单 // 注意重新开始的定时器清理由父的 Watch 或 aboutToDisappear 处理 }) Button(返回主菜单).onClick(() { this.gameState GameState.IDLE; // 改 Link }) } } }拆解片段含义Link装饰器标记「双向绑定父的 State」gameState变量名必须与父传参键名一致GameState类型与父 State 一致2.2 父组件用 $val 传参Entry Component struct Index { State gameState: GameState GameState.IDLE; State score: number 0; build() { Stack() { if (this.gameState ! GameState.IDLE) { this.GameView() } if (this.gameState GameState.PAUSED) { PauseOverlay({ gameState: this.$gameState, // ← $val 传双向引用 score: this.score // 普通值传 Prop }) } } } }关键语法this.$gameState——$前缀传「State 的引用」子组件用 Link 接收。普通this.gameState传值Propthis.$gameState传引用Link。2.3 $val 的本质this.$gameState // 引用子改会影响父 this.gameState // 值子改不影响父传递方式子装饰器数据流this.$gameStateLink双向子改父同步this.gameStateProp单向只读关键经验Link 必须用$val传引用——传普通值会编译报错或退化为 Prop。三、双向同步机制3.1 子改父同步// 子 PauseOverlay Button(继续).onClick(() { this.gameState GameState.PLAYING; // 改 Link }) // ArkUI // 1. 子改 gameState // 2. 检测到 Link 绑定父的 State gameState // 3. 同步到父 State gameState PLAYING // 4. 父的 Watch(onGameStateChange) 触发第 39 篇 // 5. 父 build() 重渲染if (gameState PAUSED) 不再渲染 PauseOverlay关键经验子改 Link 等于改父 State——会触发父的 Watch、父的重渲染、父依赖该 state 的所有组件更新。3.2 父改子同步// 父 Index this.gameState GameState.PAUSED; // 父改 State // ArkUI // 1. 父改 gameState // 2. 检测到子 PauseOverlay 的 Link gameState 绑定本 State // 3. 同步到子 Link gameState PAUSED // 4. 子 build() 重渲染如果有依赖 gameState 的显示实战经验Link 真双向——无论父改还是子改对方都同步。3.3 双向 vs 单向对比维度Prop单向Link双向子改父❌ 不回传✅ 同步父父改子✅ 同步子✅ 同步子传递语法普通值this.score引用this.$gameState适合显示型编辑型四、完整改造PauseOverlay 拆为子组件4.1 创建 PauseOverlay 子组件新建entry/src/main/ets/components/PauseOverlay.etsimport { GameState } from ./GameTypes; Component export struct PauseOverlay { Link gameState: GameState; // 双向改父 gameState Prop score: number; // 只读显示当前得分 build() { Column() { Column() { Text(⏸️) .fontSize(48) .margin({ bottom: 12 }) Text(游戏暂停) .fontSize(24) .fontWeight(FontWeight.Bold) .fontColor(#2C3E50) .margin({ bottom: 8 }) Text(当前得分: ${this.score}) // 读 Prop .fontSize(16) .fontColor(#7F8C8D) .margin({ bottom: 24 }) // 继续按钮改 Link 切回 PLAYING Button(继续游戏) .width(80%).height(48) .fontSize(17).fontWeight(FontWeight.Bold) .fontColor(#FFFFFF).backgroundColor(#2ECC71) .borderRadius(24) .margin({ bottom: 12 }) .onClick(() { this.gameState GameState.PLAYING; // ← 改 Link }) // 重新开始改 Link 切 IDLE父 Watch 处理定时器清理 Button(重新开始) .width(80%).height(48) .fontSize(17) .fontColor(#FFFFFF).backgroundColor(#F39C12) .borderRadius(24) .margin({ bottom: 12 }) .onClick(() { this.gameState GameState.IDLE; // ← 改 Link }) // 返回主菜单改 Link 切 IDLE Button(返回主菜单) .width(80%).height(48) .fontSize(17) .fontColor(#7F8C8D).backgroundColor(#ECF0F1) .borderRadius(24) .onClick(() { this.gameState GameState.IDLE; // ← 改 Link }) } .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) } }4.2 Index 引用并 $val 传参// 来源entry/src/main/ets/pages/Index.ets改造后 import { PauseOverlay } from ../components/PauseOverlay; import { GameHUD } from ../components/GameHUD; import { Cat, CatLevel, GameConfig, CatConfig, GameState, ComboInfo } from ../components/GameTypes; import { GameEngine } from ../components/GameEngine; Entry Component struct Index { State Watch(onGameStateChange) gameState: GameState GameState.IDLE; State score: number 0; State cats: Cat[] []; State combo: ComboInfo { count: 0, multiplier: 1, lastMergeTime: 0 }; State nextCatLevel: CatLevel CatLevel.SMALL; State highScore: number 0; State gameTime: number 0; State maxCombo: number 0; State mergeCount: number 0; State highestLevel: CatLevel CatLevel.SMALL; 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]; // gameState 变化回调第 39 篇处理定时器清理等副作用 onGameStateChange(newVal: GameState): void { switch (newVal) { case GameState.IDLE: // 切回主菜单时清定时器重新开始/返回主菜单都会走这里 this.clearTimers(); break; case GameState.GAME_OVER: // 结束时清定时器 this.clearTimers(); break; } } /* startGame / pauseGame / resumeGame / endGame / handleColumnClick / clearTimers / formatTime / aboutToDisappear 等略 */ build() { Stack() { if (this.gameState GameState.IDLE) { this.MainMenuView() } else { this.GameView() } // 改造用子组件替代 Builder PauseOverlay if (this.gameState GameState.PAUSED) { PauseOverlay({ gameState: this.$gameState, // ← $val 传双向引用 score: this.score // 普通值传 Prop }) } if (this.gameState GameState.GAME_OVER) { this.GameOverOverlay() } } .width(100%).height(100%) } /* GameView / MainMenuView / GameOverOverlay / StatItem 筥略 */ }4.3 改造要点原写法改造后效果this.resumeGame()调父方法this.gameState PLAYING改 Link子直接改父 statethis.clearTimers(); this.startGame()父处理this.gameState IDLE改 Link父 Watch 处理清理PauseOverlay 在 Index 内独立PauseOverlay.ets分文件维护关键经验Link 父 Watch 是「子改父 state父处理副作用」的标准模式——子只改 state副作用清定时器、播音效由父 Watch 集中处理。五、Link vs Prop vs Event 三选5.1 三种跨组件数据流完整对比装饰器方向读写传递语法适合Prop父→子只读this.score显示型HUD、卡片Link父↔子双向this.$gameState编辑型表单、弹窗按钮Event子→父事件回调this.onScoreAdd触发父方法按钮点击5.2 场景对照// 场景 1HUD 显示得分 → Prop GameHUD({ score: this.score }) // 场景 2暂停弹窗改 gameState → Link PauseOverlay({ gameState: this.$gameState }) // 场景 3子按钮点击让父加 10 → Event GameHUD({ score: this.score, onScoreAdd: (d) this.score d })5.3 Link vs Event 的细微取舍「子改父 state」既可以用 Link 直接改也可以用 Event 回调让父改// 方式 ALink 直接改 Component struct PauseOverlay { Link gameState: GameState; build() { Button(继续).onClick(() { this.gameState GameState.PLAYING; // 直接改 }) } } // 方式 BEvent 回调父改 Component struct PauseOverlay { Event onResume: () void; build() { Button(继续).onClick(() { this.onResume(); // 回调父父改 gameState }) } } // 父PauseOverlay({ onResume: () { this.gameState GameState.PLAYING; } })方式优点缺点Link 直接改简洁、少一层回调子要懂父 state 语义Event 回调子不耦合父 state多一层回调代码关键经验「子改父单一 state」用 Link 更简洁——「子触发父复杂逻辑」用 Event 更解耦。六、踩坑提示6.1 忘用 $val 传引用// ❌ 错误传普通值Link 退化为 Prop 或报错 PauseOverlay({ gameState: this.gameState }) // 传值不是引用 // ✅ 正确$val 传引用 PauseOverlay({ gameState: this.$gameState }) // $val 传引用6.2 Link 类型与父 State 不一致// 父 State gameState: GameState GameState.IDLE; // ❌ 错误子 Link 声明 string Link gameState: string; // ✅ 正确类型一致 Link gameState: GameState;6.3 子改 Link 期望触发父方法// ❌ 错误子改 Link 期望自动调父的 resumeGame() this.gameState GameState.PLAYING; // 只改 state不调方法 // ✅ 正确父用 Watch 监听 state 变化在回调里调方法 // 父 State Watch(onGameStateChange) gameState: GameState GameState.IDLE; onGameStateChange(newVal: GameState): void { if (newVal GameState.PLAYING) { this.resumeGame(); // 父在 Watch 里调方法 } } ### 6.4 Link 对象改内部属性不触发 ts // ❌ 错误改 Link 对象内部属性不触发双向同步 this.combo.count 5; // Link 也是浅观察 // ✅ 正确重新赋值整个对象 this.combo { count: 5, multiplier: 3, lastMergeTime: Date.now() };七、调试技巧console.info在子改 Link 后log 父的this.gameState追是否同步。子改了父没变排查检查是否用$val传引用检查类型是否一致检查对象是否整体赋值。父 Watch 没触发排查检查父 State 是否有 Watch检查回调字符串是否拼错。DevEco ArkUI Inspector查看组件树和 Link 绑定关系。八、性能与最佳实践编辑型子组件用 Link——弹窗按钮改父 state、表单输入改父数据。$val 传引用——普通值传 Prop引用传 Link。类型与父 State 一致——number 配 numberGameState 配 GameState。子改 Link 父 Watch 处理副作用——子只改 state副作用由父集中处理。Link 对象整体赋值才同步——和 State/Prop 一样浅观察。「子改单一 state」用 Link「子触发父复杂逻辑」用 Event——按耦合度选。总结本篇我们从 Link 双向绑定切入掌握了**Link 声明与val 传引用**、**双向同步机制子改父同步父改子同步**、**Link 父 Watch 标准模式**、**Link vs Prop vs Event 取舍**四大要点并给出了 PauseOverlay 拆为子组件的完整改造代码。核心要点**val 传引用才双向子改 Link 触发父 Watch编辑型用 Link 显示型用 Prop对象整体赋值才同步**。下一篇我们将拆解 Provide/Consume——跨层隐式共享。如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力相关资源「猫猫大作战」项目源码本仓库entry/src/main/ets/pages/Index.ets、entry/src/main/ets/components/PauseOverlay.etsArkUI Link 双向绑定官方指南ArkUI 状态管理概述ArkUI 组件化与数据流最佳实践开源鸿蒙跨平台社区HarmonyOS 开发者官方文档首页系列索引本仓库articles/INDEX.md