当前位置: 首页 > news >正文

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——用@Link

@Component 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 子组件声明 @Link

@Component 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传值(@Prop),this.$gameState传引用(@Link)。

2.3 $val 的本质

this.$gameState // 引用,子改会影响父 this.gameState // 值,子改不影响父
传递方式子装饰器数据流
this.$gameState@Link双向(子改父同步)
this.gameState@Prop单向(只读)

关键经验@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.ets

import { 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子直接改父 state
this.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 场景对照

// 场景 1:HUD 显示得分 → @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 回调让父改:

// 方式 A:@Link 直接改 @Component struct PauseOverlay { @Link gameState: GameState; build() { Button('继续').onClick(() => { this.gameState = GameState.PLAYING; // 直接改 }) } } // 方式 B:@Event 回调父改 @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() };

七、调试技巧

  1. console.info在子改 @Link 后:log 父的this.gameState,追是否同步。
  2. 子改了父没变排查:检查是否用$val传引用;检查类型是否一致;检查对象是否整体赋值。
  3. 父 @Watch 没触发排查:检查父 @State 是否有 @Watch;检查回调字符串是否拼错。
  4. DevEco ArkUI Inspector:查看组件树和 @Link 绑定关系。

八、性能与最佳实践

  1. 编辑型子组件用 @Link——弹窗按钮改父 state、表单输入改父数据。
  2. $val 传引用——普通值传 @Prop,引用传 @Link。
  3. 类型与父 @State 一致——number 配 number,GameState 配 GameState。
  4. 子改 @Link + 父 @Watch 处理副作用——子只改 state,副作用由父集中处理。
  5. @Link 对象整体赋值才同步——和 @State/@Prop 一样浅观察。
  6. 「子改单一 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.etsentry/src/main/ets/components/PauseOverlay.ets
  • ArkUI @Link 双向绑定官方指南
  • ArkUI 状态管理概述
  • ArkUI 组件化与数据流最佳实践
  • 开源鸿蒙跨平台社区
  • HarmonyOS 开发者官方文档首页
  • 系列索引:本仓库articles/INDEX.md
http://www.jsqmd.com/news/1278081/

相关文章:

  • Suno采样拼接技术解析:从原理到音乐制作实战
  • 如何快速上手NDS游戏资源编辑器:Tinke完整使用指南
  • 基于Arduino与模拟反馈舵机的简易机械臂闭环控制实践
  • Agentic RAG技术解析:从原理到企业级实践
  • 5分钟快速上手Pixelle-Video:免费AI短视频引擎终极指南
  • 图卷积网络实战:从理论到TensorFlow实现深度解析
  • 基于Arduino与AMG8833的简易热成像仪制作全攻略
  • 如何在15分钟内使用ThinkAdmin构建企业级后台管理系统完整指南
  • 基于Cortex-M0与VL53L0X的复古APPLE II互动装置设计与实现
  • Docker Desktop汉化技术深度解析:从界面本地化到自动化翻译架构实战指南
  • 从零构建鱼缸自动水位控制器:硬件选型、编程实现与避坑指南
  • HarmonyOS应用开发实战:猫猫大作战-浅观察陷阱与嵌套对象更新
  • (2026最新)广州本地漏水检测维修公司靠谱推荐:正规防水补漏上门维修-墙面/屋顶/外墙/暗管漏水检测精准定位 - 即刻修防水
  • Java+Vue+SpringBoot课程作业管理系统毕业设计:从环境搭建到答辩部署全流程实战
  • 2026 年至今,威海靠谱的靠谱的草坪基地厂家哪家强,选对这地方,草坪铺完三年没秃一片,找这类基地得擦亮眼睛 - 企业官方推荐【认证】
  • OpenClaw与Codex对比:玩具与生产级工具的差异
  • 戴森球计划工厂蓝图完全指南:从新手到高手的终极工厂建设方案
  • 魔曰加密工具终极指南:3步实现古文风文本安全防护
  • AI论文写作工具对比:千笔与灵感AI实测解析
  • 掌控板教学应用设计:从硬件到跨学科探究的实践指南
  • HarmonyOS应用开发实战:猫猫大作战-onTouch 三阶段触发、TouchType 类型判定、TouchObject 坐标信息、与 onCli
  • 基于Arduino与超声波传感器的智能感应灯制作与优化指南
  • Claude 金融 Skills 开源:从通用对话走向专业投研
  • AI工具提升论文写作效率:文献综述到格式校对的智能解决方案
  • Apache Wicket 10.10.0 发布:无 API 中断,修复多 Bug 并实现多项改进
  • UVa 10704交通问题:k短路算法实现与优化
  • HarmonyOS应用开发实战:猫猫大作战-`displayPriority` 的优先级机制、隐藏触发条件、与 layoutWeight 搭配
  • 华为MetaERP财务共享模块的技术架构与实战解析
  • ChatTTS-ui终极指南:5分钟打造本地文字转语音系统
  • 手机速度突然变慢---------原来只是因为手机发热