news 2026/7/28 23:35:06

HarmonyOS应用开发实战:猫猫大作战-HUD 拆成独立子组件、用 `@Prop` 接收父组件得分/时间为锚点,把 @Prop 声明与传递、单向只

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HarmonyOS应用开发实战:猫猫大作战-HUD 拆成独立子组件、用 `@Prop` 接收父组件得分/时间为锚点,把 @Prop 声明与传递、单向只

前言

前面我们用@State让数据驱动 UI,所有状态都在Index这一个组件里。但实战中组件要拆分——HUD、棋盘、底部栏各自独立,才好维护复用。拆分后遇到第一个问题:父组件的得分怎么传给子组件 HUD?@State只在当前组件有效,跨组件要用@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 拆成独立组件GameHUD

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

@Component 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 子组件不能改 @Prop

@Component 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.ets

import { 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.score@Prop 接收 + 父传值
复用性❌ 只能在 Index 用✅ 任何组件可引用
可维护性❌ Index 越来越臃肿✅ 分文件维护
单向数据流❌ 子可直接改父 state✅ @Prop 只读,强制单向

五、@Prop vs @Link vs @Event

ArkUI 三种跨组件数据流,容易混淆:

5.1 对比表

装饰器方向读写适合
@Prop父→子只读显示型子组件(HUD、卡片)
@Link父↔子双向子要改父数据(表单输入)
@Event子→父事件回调子触发父方法(按钮点击)

5.2 场景对照

// 场景 1:HUD 显示得分 → @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 同步新副本

七、调试技巧

  1. console.info在子 build 首行:logthis.score,追 @Prop 接收的值。
  2. 子不刷新排查:检查父是否重新赋值(对象/数组要整体赋值);检查类型是否匹配;检查是否忘传值。
  3. 子改了不回传排查:@Prop 本就单向,要回传用 @Event 或 @Link。
  4. DevEco ArkUI Inspector:查看组件树和 @Prop 绑定。

八、性能与最佳实践

  1. 显示型子组件用 @Prop——HUD、卡片、列表项等只读显示。
  2. 所有 @Prop 都要父传值——忘传子是 undefined。
  3. 类型与父 @State 一致——number 配 number,ComboInfo 配 ComboInfo。
  4. 对象/数组整体赋值才同步——@Prop 和 @State 都是浅观察。
  5. 子改数据用 @Event 或 @Link——@Prop 只读,改了不回传。
  6. @Prop 浅拷贝对象——子改副本不影响父,但也不触发重渲染。

九、阶段二进度小结(31-40)

本篇是阶段二「状态管理 + 交互 + 动画」第 10 篇,进度小结:

主题核心要点
31@State响应式状态,改变触发重渲染
32多 @State同帧批量更新,一次重渲染
33setInterval 主循环100ms 物理周期,暂停短路
34计时器1000ms 秒级,formatTime 格式化
35spawnTimer2000ms 自动生成,随机列选择
36clearTimers-1 哨兵,三时机清理,aboutToDisappear 兜底
37onClick 列投放闭包捕获 col,handleColumnClick 三步
38箭头函数 this回调统一箭头保留 this,普通函数打断链
39@Watch状态变化副作用集中,防递归守卫
40(本篇)@Prop父子单向只读,浅拷贝,显示型子组件

接下来第 41-50 篇会进入 V1 状态管理深水区:@Link 双向、@Provide/@Consume 跨层、@Observed+@ObjectLink 深观察、数组项替换、批量更新、嵌套陷阱,然后 V2 迁移:@Local、@Param、@Event、@ObservedV2+@Trace、@Monitor、AppStorageV2、PersistenceV2。

总结

本篇我们从 @Prop 父子单向切入,掌握了**@Prop 声明与父传值语法**、单向只读语义(子改不回传)浅拷贝与浅观察@Prop vs @Link vs @Event 取舍四大要点,并给出了 HUD 拆为子组件的完整改造代码。核心要点:显示型子组件用 @Prop;所有 @Prop 都要父传值;对象整体赋值才同步;子改数据用 @Event 或 @Link

下一篇我们将拆解 @Link——父子双向绑定。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

  • 「猫猫大作战」项目源码:本仓库entry/src/main/ets/pages/Index.etsentry/src/main/ets/components/GameTypes.ets
  • ArkUI @Prop 父子单向官方指南
  • ArkUI 状态管理概述
  • ArkUI 组件化与数据流最佳实践
  • 开源鸿蒙跨平台社区
  • HarmonyOS 开发者官方文档首页
  • 系列索引:本仓库articles/INDEX.md
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/28 23:33:47

3个步骤掌握MouseClick:从零开始实现鼠标自动化点击

3个步骤掌握MouseClick:从零开始实现鼠标自动化点击 【免费下载链接】MouseClick 🖱️ MouseClick 🖱️ 是一款功能强大的鼠标连点器和管理工具,采用 QT Widget 开发 ,具备跨平台兼容性 。软件界面美观 ,操…

作者头像 李华
网站建设 2026/7/28 23:32:28

计算机毕业设计之BBS论坛系统

本系统为用户而设计制作BBS论坛系统,旨在实现BBS论坛智能化、现代化管理。本BBS论坛自动化系统的开发和研制的最终目的是将BBS论坛的运作模式从手工记录数据转变为网络信息查询管理,从而为现代管理人员的使用提供更多的便利和条件。使BBS论坛系统数字化、…

作者头像 李华
网站建设 2026/7/28 23:32:18

Codex本地部署与DeepSeek接入:图形化配置实现AI编程助手低成本化

最近在折腾 AI 编程工具,发现 Codex 确实好用,但默认只能用 OpenAI 的模型,对国内开发者来说,账号、网络、费用都是不小的门槛。有没有办法让它用上 DeepSeek 这类国内模型呢?答案是肯定的。经过一番摸索,我整理了一套完整的 Codex 本地部署与 DeepSeek 接入方案,全程图…

作者头像 李华
网站建设 2026/7/28 23:29:36

PSO与DWA融合的无人机路径规划实战

1. 项目概述去年夏天我在参与一个山区物资运输项目时,遇到了一个棘手的问题:无人机在复杂地形中频繁发生碰撞事故。当时我们尝试了多种传统路径规划算法,但都无法有效应对突发障碍物。这个问题促使我开始研究将粒子群算法(PSO)与动态窗口法(D…

作者头像 李华