HarmonyOS应用开发实战:猫猫大作战-HUD 拆成独立子组件、用 `@Prop` 接收父组件得分/时间为锚点,把 @Prop 声明与传递、单向只
前言前面我们用State让数据驱动 UI所有状态都在Index这一个组件里。但实战中组件要拆分——HUD、棋盘、底部栏各自独立才好维护复用。拆分后遇到第一个问题父组件的得分怎么传给子组件 HUDState只在当前组件有效跨组件要用Prop单向只读或Link双向。本篇以「猫猫大作战」把 HUD 拆成独立子组件、用Prop接收父组件得分/时间为锚点把Prop 声明与传递、单向只读语义、Prop 的浅拷贝、Prop vs Link 取舍四大要点讲透。提示本系列不讲 ArkTS 基础语法与环境搭建假设你已跟完第 1–39 篇。本篇是阶段二第十篇。一、场景拆解HUD 拆分子组件回顾「猫猫大作战」HUD第 11、23、24 篇是Index内的Builder// 来源entry/src/main/ets/pages/Index.ets Entry Component struct Index { State score: number 0; State combo: ComboInfo { /* ... */ }; State gameTime: number 0; Builder GameHUD() { Row() { Column() { Text(得分); Text(this.score.toString()) } // ← 直接读 Index 的 State /* ... 连击、时间 */ } } }痛点HUD 逻辑和Index主逻辑混在一起Index越来越臃肿。想把 HUD 拆成独立组件GameHUDComponent struct GameHUD { Prop score: number; // ← 从父接收 Prop combo: ComboInfo; Prop gameTime: number; build() { Row() { Column() { Text(得分); Text(this.score.toString()) } // 读 Prop /* ... */ } } }父组件Index传值GameHUD({ score: this.score, combo: this.combo, gameTime: this.gameTime })关键经验Prop 父传子单向只读——父改值子同步刷新子改值不回传父。二、Prop 基本用法2.1 子组件声明 PropComponent struct GameHUD { Prop score: number; // 从父接收只读 Prop combo: ComboInfo; Prop gameTime: number; build() { Row() { Column() { Text(得分).fontSize(11).fontColor(#95A5A6) Text(this.score.toString()) // 读 Prop .fontSize(22).fontWeight(FontWeight.Bold).fontColor(#2C3E50) } /* ... 连击、时间 */ } } }拆解片段含义Prop装饰器标记「从父接收只读数据」score变量名number类型必须与父传值类型一致关键约束Prop 不需要初始值——值由父传入不赋默认。但 ArkTS 严格模式可能要显式类型。2.2 父组件传值Entry Component struct Index { State score: number 0; State combo: ComboInfo { count: 0, multiplier: 1, lastMergeTime: 0 }; State gameTime: number 0; build() { Column() { GameHUD({ score: this.score, // 传 State score combo: this.combo, // 传 State combo gameTime: this.gameTime // 传 State gameTime }) /* ... 棋盘、底部栏 */ } } }传递语法子组件名({ prop1: 父值1, prop2: 父值2 })——类似 React 的 props。2.3 单向同步机制// 父改 State子 Prop 自动同步 this.score 99; // ArkUI // 1. Index 的 State score 99 // 2. 检测到 GameHUD 的 Prop score 依赖父 State score // 3. 把 99 同步到子 Prop score // 4. 子组件 build() 重渲染Text(99)关键经验Prop 是「父改子同步子改父不回」的单向流——数据从父流向子子组件不能改 Prop 反向影响父。三、Prop 的只读语义3.1 子组件不能改 PropComponent struct GameHUD { Prop score: number; build() { Button(加 10).onClick(() { this.score 10; // ❌ Prop 只读改了不回传父且可能ArkUI 警告 }) } }为什么只读Prop是「父数据的子副本」子改了不回传父会导致父子数据不一致——ArkUI 为保证数据流单向子改 Prop 不影响父且子下次重渲染会被父值覆盖。3.2 子要改数据用 Event 回调父子组件想「加 10」要通过回调让父改Component struct GameHUD { Prop score: number; Event onScoreAdd: (delta: number) void; // 事件回调第 49 篇专讲 build() { Button(加 10).onClick(() { this.onScoreAdd(10); // 触发事件让父改 State score }) } } // 父 Entry Component struct Index { State score: number 0; build() { GameHUD({ score: this.score, onScoreAdd: (delta: number) { this.score delta; } // 父改 State }) } }关键经验子改数据用 Event 回调父Prop 只用于显示——这是单向数据流的标准模式。本系列第 49 篇会专讲 Event。3.3 Prop 的浅拷贝Prop combo: ComboInfo; // 接收对象机制Prop对父传入的对象做浅拷贝——子得到一份副本改副本内部属性不影响父但改了也不触发子重渲染Prop 浅观察。// 父 this.combo { count: 5, multiplier: 3, lastMergeTime: Date.now() }; // 子 Prop combo 收到新副本重渲染 // 子改 Prop combo 内部属性 this.combo.count 10; // ⚠️ 不触发子重渲染且不回传父 // 父的 combo.count 还是 5实战经验Prop 对象当作只读快照用——别改内部属性要改让父改整个对象传入。四、完整改造HUD 拆为子组件4.1 创建 GameHUD 子组件新建entry/src/main/ets/components/GameHUD.etsimport { ComboInfo } from ./GameTypes; import { formatTime } from ./GameUtils; // 假设有工具函数 Component export struct GameHUD { Prop score: number; Prop combo: ComboInfo; Prop gameTime: number; build() { Row() { // 左得分 Column() { Text(得分) .fontSize(11).fontColor(#95A5A6) Text(this.score.toString()) .fontSize(22).fontWeight(FontWeight.Bold).fontColor(#2C3E50) } .layoutWeight(2) .displayPriority(1) .alignItems(HorizontalAlign.Start) // 中连击条件渲染 if (this.combo.count 1) { Row() { Text( x${this.combo.multiplier}) .fontSize(18).fontWeight(FontWeight.Bold).fontColor(#E74C3C) } .layoutWeight(1) .displayPriority(2) .justifyContent(FlexAlign.Center) .padding({ left: 8, right: 8, top: 4, bottom: 4 }) .backgroundColor(rgba(231, 76, 60, 0.1)) .borderRadius(16) } // 右时间 Column() { Text(时间) .fontSize(11).fontColor(#95A5A6) Text(formatTime(this.gameTime)) .fontSize(18).fontWeight(FontWeight.Medium).fontColor(#2C3E50) } .layoutWeight(2) .displayPriority(1) .alignItems(HorizontalAlign.End) } .width(100%) .padding({ left: 20, right: 20, top: 12, bottom: 8 }) } }4.2 Index 引用并传值// 来源entry/src/main/ets/pages/Index.ets改造后 import { GameHUD } from ../components/GameHUD; import { Cat, CatLevel, GameConfig, CatConfig, GameState, ComboInfo } from ../components/GameTypes; import { GameEngine } from ../components/GameEngine; import { formatTime } from ../components/GameUtils; Entry Component struct Index { State 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; State Watch(onGameStateChange) gameState: GameState GameState.IDLE; 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 / pauseGame / resumeGame / endGame / handleColumnClick / clearTimers 筁略 */ formatTime(seconds: number): string { const min Math.floor(seconds / 60); const sec seconds % 60; return ${min.toString().padStart(2, 0)}:${sec.toString().padStart(2, 0)}; } build() { Stack() { if (this.gameState GameState.IDLE) { this.MainMenuView() } else { this.GameView() } if (this.gameState GameState.PAUSED) { this.PauseOverlay() } if (this.gameState GameState.GAME_OVER) { this.GameOverOverlay() } } .width(100%).height(100%) } Builder GameView() { Column() { // 改造用子组件替代 Builder GameHUD GameHUD({ score: this.score, // 传 State combo: this.combo, gameTime: this.gameTime }) Column() { /* 预告区、棋盘 */ }.alignItems(HorizontalAlign.Center) Spacer() /* 底部控制栏 */ } .width(100%).height(100%) .linearGradient({ direction: GradientDirection.Bottom, colors: [[#E8F4F8, 0.0], [#D6EEF5, 0.5], [#C9E8F2, 1.0]] }) .alignItems(HorizontalAlign.Center) } /* MainMenuView / PauseOverlay / GameOverOverlay / StatItem 等略 */ }4.3 改造对比维度原 Builder 版改造 Prop 子组件版HUD 代码位置Index内的 Builder独立GameHUD.ets文件数据传递直接读this.scoreProp 接收 父传值复用性❌ 只能在 Index 用✅ 任何组件可引用可维护性❌ Index 越来越臃肿✅ 分文件维护单向数据流❌ 子可直接改父 state✅ Prop 只读强制单向五、Prop vs Link vs EventArkUI 三种跨组件数据流容易混淆5.1 对比表装饰器方向读写适合Prop父→子只读显示型子组件HUD、卡片Link父↔子双向子要改父数据表单输入Event子→父事件回调子触发父方法按钮点击5.2 场景对照// 场景 1HUD 显示得分 → Prop只读 GameHUD({ score: this.score }) // 场景 2表单子组件改父的 username → Link双向 TextInput({ value: this.username }) // 内部用 Link 同步 // 场景 3子按钮点击让父加 10 → Event回调 GameHUD({ score: this.score, onScoreAdd: (d) this.score d })关键经验显示型子组件用 Prop改父数据用 Link触发父方法用 Event。本系列第 41、49 篇会专讲 Link、Event。5.3 取舍决策树子组件要改父数据 ├─ 是 → 子要改父的 State ├─ 是 → Link双向 └─ 否 → Event回调让父改 └─ 否 → Prop只读显示六、踩坑提示6.1 忘记父传值// ❌ 错误没传 score子 Prop 是 undefined GameHUD({ combo: this.combo, gameTime: this.gameTime }) // 子 Text(this.score.toString()) → Text(undefined) // ✅ 正确所有 Prop 都传 GameHUD({ score: this.score, combo: this.combo, gameTime: this.gameTime })6.2 类型不匹配// 父 State score: number 0; // ❌ 错误子 Prop 声明 string父传 number Component struct GameHUD { Prop score: string; // 类型不匹配 } // ✅ 正确类型一致 Component struct GameHUD { Prop score: number; // 与父 State 类型一致 }6.3 子改 Prop 期望回传父// ❌ 错误改 Prop 期望影响父 Component struct GameHUD { Prop score: number; build() { Button(加 10).onClick(() { this.score 10; // 改 Prop不回传父 }) } } // ✅ 正确用 Event 回调父 Component struct GameHUD { Prop score: number; Event onScoreAdd: (delta: number) void; build() { Button(加 10).onClick(() { this.onScoreAdd(10); }) } }6.4 对象改内部属性不触发重渲染// ❌ 错误父改 combo.count子 Prop combo 不重渲染 this.combo.count 5; // State 浅观察不触发子也不同步 // ✅ 正确父重新赋值整个 combo this.combo { count: 5, multiplier: 3, lastMergeTime: Date.now() }; // 父 State 触发子 Prop 同步新副本七、调试技巧console.info在子 build 首行logthis.score追 Prop 接收的值。子不刷新排查检查父是否重新赋值对象/数组要整体赋值检查类型是否匹配检查是否忘传值。子改了不回传排查Prop 本就单向要回传用 Event 或 Link。DevEco ArkUI Inspector查看组件树和 Prop 绑定。八、性能与最佳实践显示型子组件用 Prop——HUD、卡片、列表项等只读显示。所有 Prop 都要父传值——忘传子是 undefined。类型与父 State 一致——number 配 numberComboInfo 配 ComboInfo。对象/数组整体赋值才同步——Prop 和 State 都是浅观察。子改数据用 Event 或 Link——Prop 只读改了不回传。Prop 浅拷贝对象——子改副本不影响父但也不触发重渲染。九、阶段二进度小结31-40本篇是阶段二「状态管理 交互 动画」第 10 篇进度小结篇主题核心要点31State响应式状态改变触发重渲染32多 State同帧批量更新一次重渲染33setInterval 主循环100ms 物理周期暂停短路34计时器1000ms 秒级formatTime 格式化35spawnTimer2000ms 自动生成随机列选择36clearTimers-1 哨兵三时机清理aboutToDisappear 兜底37onClick 列投放闭包捕获 colhandleColumnClick 三步38箭头函数 this回调统一箭头保留 this普通函数打断链39Watch状态变化副作用集中防递归守卫40本篇Prop父子单向只读浅拷贝显示型子组件接下来第 41-50 篇会进入 V1 状态管理深水区Link 双向、Provide/Consume 跨层、ObservedObjectLink 深观察、数组项替换、批量更新、嵌套陷阱然后 V2 迁移Local、Param、Event、ObservedV2Trace、Monitor、AppStorageV2、PersistenceV2。总结本篇我们从 Prop 父子单向切入掌握了**Prop 声明与父传值语法**、单向只读语义子改不回传、浅拷贝与浅观察、Prop vs Link vs Event 取舍四大要点并给出了 HUD 拆为子组件的完整改造代码。核心要点显示型子组件用 Prop所有 Prop 都要父传值对象整体赋值才同步子改数据用 Event 或 Link。下一篇我们将拆解 Link——父子双向绑定。如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力相关资源「猫猫大作战」项目源码本仓库entry/src/main/ets/pages/Index.ets、entry/src/main/ets/components/GameTypes.etsArkUI Prop 父子单向官方指南ArkUI 状态管理概述ArkUI 组件化与数据流最佳实践开源鸿蒙跨平台社区HarmonyOS 开发者官方文档首页系列索引本仓库articles/INDEX.md

相关新闻