
前言上一篇我们拆透了定时器泄漏的坑——clearTimers兜底防泄漏。但定时器只是「时间驱动」游戏还要「事件驱动」——玩家点击某列时投放猫咪。这需要 ArkUI 的onClick事件配合透明点击层第 17 篇讲过结构和handleColumnClick处理函数第 17 篇简提本篇专讲。本篇以「猫猫大作战」列投放的事件处理为锚点把onClick 回调与闭包、handleColumnClick 处理流程、MVC 分层UI 调引擎 同步 state、投放失败的静默处理四大要点讲透。提示本系列不讲 ArkTS 基础语法与环境搭建假设你已跟完第 1–36 篇。本篇是阶段二第七篇。一、场景拆解玩家点击列投放回顾「猫猫大作战」透明点击层第 17 篇和 handleColumnClick第 17 篇简提// 来源entry/src/main/ets/pages/Index.ets GameView() 第 3 层 Row() { ForEach(this.cols, (col: number) { Column() .width(GameConfig.CELL_SIZE) .height(GameConfig.BOARD_HEIGHT * GameConfig.CELL_SIZE) .backgroundColor(rgba(0,0,0,0)) .onClick(() { this.handleColumnClick(col); // ← 闭包捕获 col }) }, (col: number) click_${col}) }// 来源entry/src/main/ets/pages/Index.ets handleColumnClick(column: number) { if (this.gameState ! GameState.PLAYING) return; // ← 状态守卫 if (this.gameEngine.dropCat(column)) { // ← 引擎投放 this.cats this.gameEngine.getAllCats(); // ← 同步猫数组 this.nextCatLevel this.gameEngine.getNextCatLevel(); // ← 更新预告 } }核心问题onClick回调里的col怎么绑定到对应列handleColumnClick的三步流程守卫→投放→同步有什么讲究dropCat返回false时列已满怎么处理UI 层和引擎层的职责怎么分二、onClick 回调与闭包2.1 闭包按值捕获 colForEach(this.cols, (col: number) { Column() .onClick(() { this.handleColumnClick(col); // col 被闭包捕获 }) }, (col: number) click_${col})机制ForEach每次迭代都创建一个新的箭头函数闭包捕获当时的col值。执行流程迭代col 值创建的闭包第 1 次0() this.handleColumnClick(0)第 2 次1() this.handleColumnClick(1)第 3 次2() this.handleColumnClick(2)第 4 次3() this.handleColumnClick(3)第 5 次4() this.handleColumnClick(4)关键经验ForEach 的迭代变量是「每次新建」的——不是 JS 经典的var共享坑每列绑定到自己的col。2.2 闭包 vs 普通函数的 this// ❌ 错误普通函数 this 不指向组件 .onClick(function () { this.handleColumnClick(col); // this 是触发事件的 Column不是 Index }) // ✅ 正确箭头函数保留外层 this .onClick(() { this.handleColumnClick(col); // this 指向 Index struct })关键经验ArkUI 事件回统一用箭头函数——保留外层this能直接调this.xxx。本系列第 38 篇会专讲箭头函数 this 绑定。2.3 onClick 触发时机ArkUI 的onClick是点击抬起时触发down → up 算一次 click手势阶段触发回调手指按下downonTouch的Down手指移动moveonTouch的Move手指抬起uponClick触发实战经验长按要响应用onTouch 计时器单纯点击用onClick就够。本系列第 100 篇会专讲 Menu 长按菜单。三、handleColumnClick 三步流程3.1 步骤一状态守卫handleColumnClick(column: number) { if (this.gameState ! GameState.PLAYING) return; // 仅 PLAYING 接受点击 /* ... */ }守卫的作用gameStatehandleColumnClick 行为原因IDLE短路返回主菜单态不投放PLAYING正常投放游戏中接受点击PAUSED短路返回暂停态不接受操作GAME_OVER短路返回结束态不投放关键经验UI 层守卫比引擎层守卫更省——if短路避免无效的dropCat调用省一次引擎遍历。3.2 步骤二引擎投放if (this.gameEngine.dropCat(column)) { /* ... 同步 state */ }dropCat引擎实现第 121 篇会专讲重力下落此处简提// 来源entry/src/main/ets/components/GameEngine.ets dropCat(column: number): boolean { if (column 0 || column GameConfig.BOARD_WIDTH) return false; // 越界守卫 // 检查该列顶部是否有空间 if (this.board[0][column] ! null) return false; // 列满守卫 const cat: Cat { id: cat_${this.catCounter}, level: this.nextCatLevel, x: column, y: 0, // 从顶部投放 falling: true // 标记下落态 }; this.cats.set(cat.id, cat); // 生成下一只预览 this.nextCatLevel this.randomLevel(); return true; }dropCat 双重守卫守卫作用column 0this.board[0][column] ! null防列满顶部已有猫返回值返回含义UI 层行为true投放成功同步 cats nextCatLevelfalse投放失败越界/列满静默忽略不更新 UI3.3 步骤三同步 stateif (this.gameEngine.dropCat(column)) { this.cats this.gameEngine.getAllCats(); // 同步猫数组 this.nextCatLevel this.gameEngine.getNextCatLevel(); // 更新预告 }为什么同步两个 statestate同步原因this.cats投放后猫数组增 1UI 要渲染新猫this.nextCatLeveldropCat 内部this.nextCatLevel this.randomLevel()生成了新预览UI 要显示关键经验投放后必同步两个 state——漏cats猫不显示漏nextCatLevel预告区不准。四、MVC 分层UI 调引擎 同步 state4.1 职责分层UI 层Index.ets ├─ onClick 回调闭包捕获 col └─ handleColumnClick守卫 → 调引擎 → 同步 state ↓ 引擎层GameEngine.ets └─ dropCat越界守卫 → 列满守卫 → 创建 Cat → 加入 Map ↓ 数据层 ├─ this.catsMapstring, Cat └─ this.board二维数组 **分层原则** | 层 | 职责 | 不做的事 | |----|------|----------| | UI 层 | 事件分发、状态守卫、state 同步 | 直接改棋盘、算物理 | | 引擎层 | 棋盘操作、物理计算、规则判定 | 渲染、事件处理 | | 数据层 | 存储猫和棋盘 | 逻辑 | **关键经验****UI 层只调引擎 同步 state不直接改棋盘**——这是 MVC 分层的最小实践。本系列第 111 篇会专讲 GameEngine 引擎类。 ### 4.2 UI 层的边界 ts // ❌ 错误UI 层直接改棋盘绕过引擎 handleColumnClick(column: number) { if (this.gameState ! GameState.PLAYING) return; // 直接创建猫绕过引擎 const cat: Cat { id: cat_${Date.now()}, level: this.nextCatLevel, x: column, y: 0, falling: true }; this.cats.push(cat); // UI 层直接改数组 // 但引擎的 cats Map 和 board 二维数组没更新物理循环会错乱 } // ✅ 正确UI 层调引擎引擎改数据UI 同步快照 handleColumnClick(column: number) { if (this.gameState ! GameState.PLAYING) return; if (this.gameEngine.dropCat(column)) { // 引擎改数据 this.cats this.gameEngine.getAllCats(); // UI 同步快照 this.nextCatLevel this.gameEngine.getNextCatLevel(); } }4.3 引擎是单一数据源// UI 的 this.cats 只是引擎 Map 的快照 this.cats this.gameEngine.getAllCats(); // Array.from(this.cats.values()) // 引擎内部数据 private board: (Cat | null)[][] []; // 二维棋盘 private cats: Mapstring, Cat new Map(); // id → Cat 映射关键经验引擎是单一数据源——board和catsMap 由引擎统一管UI 的this.cats只是快照。多次同步无竞态都从引擎读。五、投放失败的静默处理5.1 列已满时 dropCat 返回 falseif (this.gameEngine.dropCat(column)) { /* 投放成功同步 state */ } // 投放失败返回 falseif 不通过啥也不做 **静默处理的原因** | 处理方式 | 用户体验 | 适合 | |----------|----------|------| | **静默忽略本项目** | 点击没反应自己看棋盘知道列满 | 消除类游戏 | | Toast 提示 | 「该列已满」 | 新手友好 | | 振动反馈 | 短振一下 | 触觉反馈型 | **关键经验****消除类游戏列满是常态静默处理最不打扰**——玩家看到列顶部有猫就知道满了不需要每次提示。 ### 5.2 进阶Toast 投放失败提示 ts import { promptAction } from kit.ArkUI; handleColumnClick(column: number) { if (this.gameState ! GameState.PLAYING) return; if (this.gameEngine.dropCat(column)) { this.cats this.gameEngine.getAllCats(); this.nextCatLevel this.gameEngine.getNextCatLevel(); } else { // 投放失败提示 promptAction.showToast({ message: 该列已满, duration: 800, bottom: 40% }); } }实战经验新手模式加 Toast高手模式静默——可用配置切换。本系列第 95 篇会专讲 Toast 提示。5.3 越界守卫的兜底// 引擎内越界守卫 dropCat(column: number): boolean { if (column 0 || column GameConfig.BOARD_WIDTH) return false; /* ... */ }为什么需要越界守卫handleColumnClick的column来自ForEach(this.cols, ...)cols [0,1,2,3,4]理论上不会越界。但防御式编程万一未来 cols 数组改错引擎的越界守卫兜底不会数组越界崩溃。关键经验系统边界做输入校验——UI 层传入的数据不可信引擎层兜底校验。六、完整代码onClick 列投放// 来源entry/src/main/ets/pages/Index.ets Entry Component struct Index { State gameState: GameState GameState.IDLE; State cats: Cat[] []; State nextCatLevel: CatLevel CatLevel.SMALL; /* ... 其他 state */ private gameEngine: GameEngine new GameEngine(); private readonly cols: number[] [0, 1, 2, 3, 4]; // 列投放处理函数本篇重点 handleColumnClick(column: number) { // 步骤一状态守卫 if (this.gameState ! GameState.PLAYING) return; // 步骤二引擎投放 步骤三同步 state if (this.gameEngine.dropCat(column)) { this.cats this.gameEngine.getAllCats(); this.nextCatLevel this.gameEngine.getNextCatLevel(); } // 投放失败静默忽略 } Builder GameView() { Column() { this.GameHUD() Column() { // 预告区 Row() { /* ... */ } // 游戏棋盘 Stack3 层叠 Stack() { // 第 1 层棋盘背景网格 Column() { /* ForEach rows × cols */ } // 第 2 层猫咪渲染 ForEach(this.cats, (cat: Cat) { Column() { /* ... */ } .position({ x: ..., y: ... }) .animation({ duration: 100 }) }, (cat: Cat) cat.id) // 第 3 层列点击层onClick 本篇重点 Row() { ForEach(this.cols, (col: number) { Column() .width(GameConfig.CELL_SIZE) .height(GameConfig.BOARD_HEIGHT * GameConfig.CELL_SIZE) .backgroundColor(rgba(0,0,0,0)) .onClick(() { this.handleColumnClick(col); // 闭包捕获 col }) }, (col: number) click_${col}) } } .width(GameConfig.BOARD_WIDTH * GameConfig.CELL_SIZE) .height(GameConfig.BOARD_HEIGHT * GameConfig.CELL_SIZE) .borderRadius(12) .clip(true) .backgroundColor(#D6EEF5) } .alignItems(HorizontalAlign.Center) Spacer() // 底部控制栏 Row() { /* 暂停 / 重新开始 */ } .width(100%) .padding({ left: 24, right: 24, bottom: 24, top: 12 }) } .width(100%).height(100%) .linearGradient({ direction: GradientDirection.Bottom, colors: [[#E8F4F8, 0.0], [#D6EEF5, 0.5], [#C9E8F2, 1.0]] }) .alignItems(HorizontalAlign.Center) } }七、踩坑提示7.1 onClick 用普通函数丢 this// ❌ 错误普通函数 this 不指向组件 .onClick(function () { this.handleColumnClick(col); // this 是 Column不是 Index }) // ✅ 正确箭头函数保留外层 this .onClick(() { this.handleColumnClick(col); })7.2 忘记状态守卫暂停时还能投放// ❌ 错误没守卫暂停时点击还能投放 handleColumnClick(column: number) { if (this.gameEngine.dropCat(column)) { /* ... */ } } // ✅ 正确守卫在前 handleColumnClick(column: number) { if (this.gameState ! GameState.PLAYING) return; if (this.gameEngine.dropCat(column)) { /* ... */ } }7.3 忘同步 nextCatLevel预告区不准// ❌ 错误只同步 cats没同步 nextCatLevel if (this.gameEngine.dropCat(column)) { this.cats this.gameEngine.getAllCats(); // 忘了 this.nextCatLevel this.gameEngine.getNextCatLevel(); } // 预告区显示的还是上一只的等级不准 // ✅ 正确同步两个 state if (this.gameEngine.dropCat(column)) { this.cats this.gameEngine.getAllCats(); this.nextCatLevel this.gameEngine.getNextCatLevel(); }7.4 UI 层直接改棋盘绕过引擎// ❌ 错误UI 层直接 push 猫到数组 handleColumnClick(column: number) { const cat: Cat { /* ... */ }; this.cats.push(cat); // 引擎的 board 和 cats Map 没更新 } // ✅ 正确调引擎引擎改数据 handleColumnClick(column: number) { if (this.gameEngine.dropCat(column)) { this.cats this.gameEngine.getAllCats(); this.nextCatLevel this.gameEngine.getNextCatLevel(); } }八、调试技巧console.info打 columnonClick 回调里 logclick col, col追点击分发。点击无响应排查检查gameState PLAYING检查上层是否有弹窗 Stack 挡住检查.hitTestBehavior。投放后猫不显示检查this.cats this.gameEngine.getAllCats()是否调用检查 ForEach 密钥cat.id是否冲突。预告区不准检查this.nextCatLevel this.gameEngine.getNextCatLevel()是否调用。九、性能与最佳实践onClick 用箭头函数保留 this——普通函数 this 不指向组件。ForEach 闭包按值捕获 col——每列绑定到自己的 col不共享。handleColumnClick 三步流程——守卫 → 引擎投放 → 同步 state。MVC 分层——UI 只调引擎 同步不直接改棋盘。投放失败静默处理——消除类游戏列满是常态静默最不打扰。引擎越界守卫兜底——UI 传入数据不可信引擎层校验。总结本篇我们从 onClick 列投放切入掌握了onClick 回调与闭包按值捕获 col、handleColumnClick 三步流程守卫→投放→同步、MVC 分层UI 调引擎 同步 state、投放失败的静默处理四大要点并给出了完整 onClick 列投放代码。核心要点闭包按值捕获列号UI 只调引擎不写逻辑投放失败静默引擎越界守卫兜底。下一篇我们将专讲箭头函数 this 绑定——callback 里 this 的核心机制。如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力相关资源「猫猫大作战」项目源码本仓库entry/src/main/ets/pages/Index.ets、entry/src/main/ets/components/GameEngine.etsonClick 通用点击事件官方指南ForEach 循环渲染官方指南HarmonyOS 事件处理最佳实践开源鸿蒙跨平台社区HarmonyOS 开发者官方文档首页系列索引本仓库articles/INDEX.md