HarmonyOS应用开发实战:猫猫大作战-@Link 声明与 $val 传参、双向同步机制、@Link vs @Prop vs @Event 取舍、

发布时间:2026/7/28 3:17:03

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

相关新闻

影刀RPA实战教程:7个真实案例带你从入门到独立开发

影刀RPA实战教程:7个真实案例带你从入门到独立开发

2026/7/28 3:17:03

影刀RPA实战教程:7个真实案例带你从入门到独立开发 教程看了不少,一到自己写就无从下手。这很正常——RPA是做的技能,不是看的技能。这篇直接用7个真实案例串起来,从简单到复杂,每个案例都能直接拿去用或者改成你自己…

手势控制电机:从PAJ7620传感器到Arduino的完整实现指南

手势控制电机:从PAJ7620传感器到Arduino的完整实现指南

2026/7/28 3:17:03

1. 项目概述:从“动手”到“动指”的交互革命“手势控制电机运动”,这个标题听起来像是科幻电影里的场景,但今天,它已经是我们触手可及的创客项目和工业应用原型。简单来说,这就是一套系统,它能让你的手在空…

OpenClaw AI Agent框架:从核心架构到实战部署全解析

OpenClaw AI Agent框架:从核心架构到实战部署全解析

2026/7/28 3:07:02

如果你是一位关注 AI 开源项目的开发者,最近可能频繁看到一个名字:OpenClaw。这个被社区昵称为"小龙虾"的项目,在 GitHub 上迅速获得了大量关注,但很多人对它的理解还停留在"又一个 AI Agent 框架"的层面。 …

2026最新!商标注册全流程避坑指南,这5个雷区千万别踩

2026最新!商标注册全流程避坑指南,这5个雷区千万别踩

2026/7/28 4:27:14

2026最新!商标注册全流程避坑指南,这5个雷区千万别踩2026年6月,《商标法》迎来四十余年来首次全面修订,条文由73条增至87条,将于2027年1月1日起施行-1。新法在收紧注册规则的同时,也赋予了商标管理部门更多…

商标取名总是被驳回?审查员最爱3个命名套路一次讲透

商标取名总是被驳回?审查员最爱3个命名套路一次讲透

2026/7/28 4:27:14

商标取名总是被驳回?审查员最爱的3个命名套路一次讲透在深圳,商标驳回率常年居高不下。2026年新《商标法》对商标注册的审查标准进一步收紧——从囤标拦截到显著性审查,全面从严-1-5。很多创业者抱怨:“我取的名字明明很特别&…

创业公司必看:为什么说“市场未动,商标先行

创业公司必看:为什么说“市场未动,商标先行

2026/7/28 4:27:14

创业公司必看:为什么说“市场未动,商标先行”是条铁律?“公司名想好了,产品也快上线了,注册商标?不急,等做大了再说。”这是不少深圳创业者的真实想法。但这个看似“省事”的选择,可…

EVA泡沫手工制作《我的世界》钻石镐:从材料选择到上色涂装全流程解析

EVA泡沫手工制作《我的世界》钻石镐:从材料选择到上色涂装全流程解析

2026/7/28 4:27:14

1. 项目概述:从像素到现实,一把镐子的诞生如果你和我一样,既是《我的世界》的忠实玩家,又是手工爱好者,那么看到游戏里那些标志性的工具,比如一把钻石镐,心里总会痒痒的:要是能把它从…

基于MIT App Inventor与CloudDB的实时聊天应用开发实践

基于MIT App Inventor与CloudDB的实时聊天应用开发实践

2026/7/28 4:27:13

1. 项目概述:当AI遇上信使,一次低门槛的云端对话实践最近在捣鼓一个挺有意思的小项目,叫“信使”。这其实是“Hour of AI”系列教程中的一个经典案例,核心目标很简单:用最直观的方式,让你亲手搭建一个能跨设…

Langgraph Loop Control Strategies 循环控制策略

Langgraph Loop Control Strategies 循环控制策略

2026/7/28 4:17:09

Loop Control Strategies 循环控制策略1. Counter-Based Loops&#xff5c;基于计数器的循环if state["iteration"] < MAX_ITERATIONS:return "continue"逻辑&#xff1a;依靠迭代次数限制循环规则&#xff1a;如果当前迭代次数小于最大迭代上限&#x…

[具身智能-649]:个人电脑搭建 RTSP 服务完整方案(Windows / Ubuntu 双平台,适配 RDK X5 rtsp2display 调试)

[具身智能-649]:个人电脑搭建 RTSP 服务完整方案(Windows / Ubuntu 双平台,适配 RDK X5 rtsp2display 调试)

2026/7/27 8:45:59

目标&#xff1a;电脑作为RTSP 服务端&#xff0c;循环推送 H264/H265 视频流&#xff1b; RDK X5 通过 rtsp2display 拉流预览&#xff0c;完全不需要在开发板编译 live555。 提供两套成熟方案&#xff1a; ✅ 方案 A&#xff1a;FFmpeg&#xff08;最简单&#xff0c;优先推…

PDF合并与动态水印的工程化方案:2026国内免费工具实测对比

PDF合并与动态水印的工程化方案:2026国内免费工具实测对比

2026/7/27 8:42:17

一、背景与测试方案 在实际项目交付中&#xff0c;PDF文件合并与版权保护水印的叠加是一个高频但容易被低估的技术需求。典型的处理链路涉及&#xff1a;多源PDF的文件流合并、页面级水印渲染&#xff08;含透明度混合与图层叠加&#xff09;、输出文件体积控制。看似简单的操作…

PDF拆分压完图糊了?2026国内免费实测,档案员都在用的组合方案

PDF拆分压完图糊了?2026国内免费实测,档案员都在用的组合方案

2026/7/27 14:56:57

说实话&#xff0c;提到PDF拆分再压缩&#xff0c;我真是被折腾得够呛。 上个月公司年度合同归档&#xff0c;一份300多页的PDF总合同&#xff0c;需要按年份拆分成三个独立文件&#xff0c;再分别压缩到10MB以内方便邮件发送各部门确认。我心想这还不简单&#xff1f;先找个海…

零基础搭建桌面智能体,OpenClaw 2.7.9 分步实操,避开绝大多数部署陷阱

零基础搭建桌面智能体,OpenClaw 2.7.9 分步实操,避开绝大多数部署陷阱

2026/7/28 0:06:55

&#x1f4cc; 一、工具核心优势盘点 数据本地存储&#xff0c;安全系数高所有操作日志、文档资料均保存在本机&#xff0c;不会上传至云端&#xff0c;能够有效保护企业文件与个人隐私&#xff0c;规避数据泄露风险。 上手简单&#xff0c;零编程门槛采用全图形化可视化界面&…

计算机毕业设计之基于springboot的购物平台设计与实现

计算机毕业设计之基于springboot的购物平台设计与实现

2026/7/28 0:06:55

由于移动应用技术的持续性的快速发展&#xff0c;现实生活中人们大多数都是通过移动手机、电脑等智能设备来完成生活中的事务。因此&#xff0c;许多的人工传统行业也开始与互联网结合&#xff0c;不再一味的依靠人工手动&#xff0c;努力打造半自动数字化甚至是全自动数字化模…

豆包AI绘图提示词失效真相:NLP模型层token截断机制首次披露,3招绕过字数限制

豆包AI绘图提示词失效真相:NLP模型层token截断机制首次披露,3招绕过字数限制

2026/7/28 0:06:55

更多请点击&#xff1a; https://codechina.net 第一章&#xff1a;豆包AI绘图提示词失效现象全景扫描 近期大量用户反馈&#xff0c;豆包&#xff08;Doubao&#xff09;AI绘图功能对常规提示词&#xff08;Prompt&#xff09;响应异常&#xff1a;语义明确的指令被忽略、中英…