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

HarmonyOS应用开发实战:猫猫大作战-被动批量(同帧)、主动批量(batchUpdate)、跨帧批量的陷阱、批量与深观察的协同

前言

第 32 篇我们讲过「同帧批量更新」——一个回调里改多个@State,ArkUI 合并成一次重渲染。但那是「被动批量」——靠回调天然同帧。实战中还有「主动批量」场景:连续多次逻辑操作改 state,要强制合并成一次重渲染,避免中途触发浪费。HarmonyOS 提供了batchUpdate机制(V2)和「手动批」套路(V1)。

本篇以「猫猫大作战」结束游戏时连续改 5 个 state 为锚点,把被动批量(同帧)主动批量(batchUpdate)跨帧批量的陷阱批量与深观察的协同四大要点讲透。

提示:本系列不讲 ArkTS 基础语法与环境搭建,假设你已跟完第 1–44 篇。本篇是阶段二第十五篇。

一、场景拆解:endGame 连续改 5 个 state

回顾「猫猫大作战」endGame(第 33、36 篇):

// 来源:entry/src/main/ets/pages/Index.ets endGame() { this.gameState = GameState.GAME_OVER; // 改 1 this.maxCombo = this.gameEngine.getMaxCombo(); // 改 2 this.mergeCount = this.gameEngine.getMergeCount(); // 改 3 this.highestLevel = this.gameEngine.getHighestLevel(); // 改 4 if (this.score > this.highScore) { this.highScore = this.score; // 改 5 } this.clearTimers(); }

核心问题:5 个@State连续改,ArkUI 重渲染几次?

答案1 次——因为都在endGame同一个调用栈(同帧),ArkUI 收集所有变更,下一 VSync 只 build() 一次。这是第 32 篇讲的「被动批量」。

但有种进阶坑

endGame() { this.gameState = GameState.GAME_OVER; // 改 1 this.clearTimers(); // 调方法,可能内部又改 state this.maxCombo = this.gameEngine.getMaxCombo(); // 改 2 }

clearTimers()内部如果改了@State(比如清空cats),仍然同帧批量——只要都在endGame调用栈内,不论调多少层方法,都合并。

二、被动批量:同帧合并

2.1 同帧收集机制

endGame() { this.gameState = GameState.GAME_OVER; // 标记 1 this.maxCombo = 5; // 标记 2 this.mergeCount = 10; // 标记 3 this.highestLevel = CatLevel.BIG; // 标记 4 this.highScore = 1500; // 标记 5 } // 调用栈结束 → 下一 VSync → 一次 build() 所有脏组件

机制

  1. ArkUI 在每次@State赋值时标记依赖组件为脏不立即重渲染
  2. 当前 JS 调用栈结束(endGame return)。
  3. 下一个 VSync 帧,一次性 build() 所有脏组件

关键经验「同调用栈」=「同帧」=「批量」——不论改多少 state、调多少层方法,只要在一个调用栈内都合并。

2.2 跨帧多次重渲染的浪费

// ❌ 错误:用 setTimeout 分次改,跨帧多次重渲染 endGame() { this.gameState = GameState.GAME_OVER; setTimeout(() => { this.maxCombo = 5; }, 0); // 下一帧 setTimeout(() => { this.mergeCount = 10; }, 0); // 再下一帧 setTimeout(() => { this.highScore = 1500; }, 0); // 再下一帧 } // 4 次重渲染(gameState + maxCombo + mergeCount + highScore 各一次)

对比

方式重渲染次数性能
同帧批量(endGame 内连续改)1 次✅ 最优
setTimeout(0) 分次4 次❌ 浪费 4 倍

2.3 被动批量的覆盖范围

endGame() { this.gameState = GameState.GAME_OVER; this.clearTimers(); // 内部改 state,仍同帧 this.helperA(); // 内部改 state,仍同帧 this.helperB(); // 内部改 state,仍同帧 this.highScore = 1500; } // 全部在 endGame 调用栈内,1 次重渲染 helperA() { this.maxCombo = 5; } helperB() { this.mergeCount = 10; } clearTimers() { /* 可能改 cats 等 */ }

关键经验被动批量覆盖整个调用栈——不论嵌套多少层方法,只要起点是同一个用户/定时器回调,都合并。

三、主动批量:batchUpdate

3.1 什么时候需要主动批量

被动批量已经覆盖大部分场景,但有种情况要主动批量——异步操作中途改 state

async endGame() { this.gameState = GameState.GAME_OVER; // 改 1(本次帧) const stats = await this.fetchStats(); // 异步等待 this.maxCombo = stats.maxCombo; // 改 2(下一帧) this.mergeCount = stats.mergeCount; // 改 3(再下一帧) } // gameState 单独一次重渲染,maxCombo+mergeCount 同帧一次,共 2 次

痛点await让调用栈断开,后续改 state 跨帧,无法被动批量。

3.2 batchUpdate 主动合并

HarmonyOS V2 提供batchUpdate主动批量:

import { batchUpdate } from '@kit.ArkUI'; async endGame() { this.gameState = GameState.GAME_OVER; // 改 1(本次帧) const stats = await this.fetchStats(); // 异步等待 // 主动批量:后续改合并成一次 batchUpdate(() => { this.maxCombo = stats.maxCombo; // 改 2 this.mergeCount = stats.mergeCount; // 改 3 this.highestLevel = stats.highestLevel; // 改 4 if (stats.score > this.highScore) { this.highScore = stats.score; // 改 5 } }); } // gameState 一次重渲染,batchUpdate 内 4 个改合并一次,共 2 次

拆解

片段含义
batchUpdate(() => { ... })主动批量装饰器,回调内所有 state 改动合并
回调内this.xxx = ...改的 state 都标记,但延迟到 batchUpdate 结束才刷新

关键经验batchUpdate 用于「异步后连续改 state」场景——把跨帧的改动强制合并成一次重渲染。

3.3 batchUpdate vs 被动批量

维度被动批量batchUpdate
触发同调用栈自动手动包回调
覆盖同步连续改异步后连续改
V1/V2V1 V2 都支持V2 专属
推荐同步场景异步场景

实战经验同步连续改用被动批量(不包 batchUpdate),异步后连续改用 batchUpdate

四、跨帧批量的陷阱

4.1 await 后的改动天然跨帧

async loadData() { this.loading = true; // 改 1(本次帧) const data = await fetch('/api'); // 异步等待 this.loading = false; // 改 2(下一帧) this.data = data; // 改 3(同改 2 帧) } // loading=true 单独一次重渲染,loading=false + data 同帧一次,共 2 次

机制await让函数返回,调用栈断开。await 后的代码在新微任务帧执行,与 await 前不同帧。

4.2 多个 await 的多次跨帧

async loadData() { this.loading = true; // 改 1(帧 A) const user = await fetch('/user'); // 异步 this.user = user; // 改 2(帧 B) const posts = await fetch('/posts'); // 异步 this.posts = posts; // 改 3(帧 C) } // 3 次重渲染(loading、user、posts 各一次)

优化:用 batchUpdate 合并后两个:

async loadData() { this.loading = true; const user = await fetch('/user'); const posts = await fetch('/posts'); batchUpdate(() => { this.user = user; // 改 2 this.posts = posts; // 改 3 this.loading = false; // 改 4 }); } // 2 次重渲染(loading=true、batchUpdate 内合并一次)

4.3 Promise.all 并发再批量

async loadData() { this.loading = true; const [user, posts] = await Promise.all([ fetch('/user'), fetch('/posts') ]); // 并发,只一次 await batchUpdate(() => { this.user = user; this.posts = posts; this.loading = false; }); } // 2 次重渲染(最优)

关键经验Promise.all 并发 + batchUpdate 批量 = 异步最少重渲染——并发减少 await 次数,批量减少重渲染次数。

五、批量与深观察的协同

5.1 @Observed 实例改属性也批量

// Cat 是 @Observed class(第 43 篇) endGame() { this.gameState = GameState.GAME_OVER; // 改 @State // 改 @Observed 实例属性 this.cats.forEach((cat) => { cat.falling = false; // 改每个猫的 falling }); this.score = this.gameEngine.getScore(); // 改 @State } // gameState + 所有 cat.falling + score 都同帧批量,1 次重渲染

机制@Observed实例改属性也走「标记脏 + 延迟刷新」机制,与@State共用批量队列。

5.2 深观察与被动批量混用

// 同帧改 @State 和 @Observed 属性,全部合并 endGame() { this.gameState = GameState.GAME_OVER; // @State this.cats[0].y = 7; // @Observed 属性 this.cats[0].falling = false; // @Observed 属性 this.score = 99; // @State } // 1 次重渲染,所有脏组件(GameOverOverlay、CatItem、HUD)一起刷

关键经验@State 浅观察和 @Observed 深观察共用批量队列——同帧内都合并,不论观察层级。

六、完整代码:endGame 批量更新

// 来源:entry/src/main/ets/pages/Index.ets(V1 被动批量版) @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; // 结束游戏:被动批量,5 个 state 连续改,1 次重渲染(本篇重点) endGame() { this.gameState = GameState.GAME_OVER; // 改 1 this.maxCombo = this.gameEngine.getMaxCombo(); // 改 2 this.mergeCount = this.gameEngine.getMergeCount(); // 改 3 this.highestLevel = this.gameEngine.getHighestLevel(); // 改 4 if (this.score > this.highScore) { this.highScore = this.score; // 改 5(条件) } this.clearTimers(); // 不改 state,不影响批量 // 所有改动在 endGame 调用栈内,ArkUI 下一 VSync 一次 build() // GameOverOverlay + HUD + 棋盘(如果有残留)一起重渲染 } // gameState 变化副作用(第 39 篇) onGameStateChange(newVal: GameState): void { switch (newVal) { case GameState.GAME_OVER: this.playSfx('over'); this.vibrate(); break; } } startGame() { this.clearTimers(); this.gameEngine.reset(); this.gameState = GameState.PLAYING; // 改 1 this.score = 0; // 改 2 this.cats = []; // 改 3 this.gameTime = 0; // 改 4 this.combo = { count: 0, multiplier: 1, lastMergeTime: 0 }; // 改 5 this.nextCatLevel = this.gameEngine.getNextCatLevel(); // 改 6 // 6 个 state 连续改,1 次重渲染 this.gameLoopTimer = setInterval(() => { if (this.gameState !== GameState.PLAYING) return; this.cats = this.gameEngine.updateCats(); // 改 1 this.score = this.gameEngine.getScore(); // 改 2 this.combo = this.gameEngine.getCombo(); // 改 3 if (this.gameEngine.isGameOver()) { this.endGame(); } }, 100); // 主循环每 100ms 改 3-4 个 state,每帧 1 次重渲染 /* spawnTimer、timeTimer 筥略 */ } handleColumnClick(column: number) { if (this.gameState !== GameState.PLAYING) return; if (this.gameEngine.dropCat(column)) { this.cats = this.gameEngine.getAllCats(); // 改 1 this.nextCatLevel = this.gameEngine.getNextCatLevel(); // 改 2 } // 2 个 state 同帧,1 次重渲染 } /* pauseGame / resumeGame / clearTimers / formatTime / aboutToDisappear 筥略 */ build() { Stack() { if (this.gameState === GameState.IDLE) { this.MainMenuView() } else { this.GameView() } if (this.gameState === GameState.PAUSED) { PauseOverlay({ gameState: this.$gameState, score: this.score }) } if (this.gameState === GameState.GAME_OVER) { this.GameOverOverlay() } } .width('100%').height('100%') } /* GameView / MainMenuView / GameOverOverlay / StatItem 等略 */ }

七、踩坑提示

7.1 setTimeout 分次改破坏批量

// ❌ 错误:setTimeout 分次,跨帧多次重渲染 endGame() { this.gameState = GameState.GAME_OVER; setTimeout(() => { this.maxCombo = 5; }, 0); setTimeout(() => { this.mergeCount = 10; }, 0); } // ✅ 正确:同调用栈连续改 endGame() { this.gameState = GameState.GAME_OVER; this.maxCombo = 5; this.mergeCount = 10; }

7.2 await 后忘 batchUpdate

// ❌ 错误:await 后连续改,跨帧多次重渲染 async endGame() { this.gameState = GameState.GAME_OVER; const stats = await this.fetchStats(); this.maxCombo = stats.maxCombo; // 跨帧 this.mergeCount = stats.mergeCount; // 跨帧 } // ✅ 正确:await 后用 batchUpdate async endGame() { this.gameState = GameState.GAME_OVER; const stats = await this.fetchStats(); batchUpdate(() => { this.maxCombo = stats.maxCombo; this.mergeCount = stats.mergeCount; }); }

7.3 以为 batchUpdate 是 V1

// ❌ 错误:V1 没有 batchUpdate,编译报错 import { batchUpdate } from '@kit.ArkUI'; // V1 项目可能无此导出 // ✅ 正确:V1 用被动批量(同调用栈),V2 才有 batchUpdate // V1:把异步后的改动用 Promise.all 合并,减少 await 次数 async endGame() { this.gameState = GameState.GAME_OVER; const stats = await this.fetchStats(); // 只一次 await // stats 拿到后连续改,被动批量 this.maxCombo = stats.maxCombo; this.mergeCount = stats.mergeCount; }

7.4 @Watch 触发时机误判

endGame() { this.gameState = GameState.GAME_OVER; // 触发 onGameStateChange this.maxCombo = 5; // 改 state } // onGameStateChange 在 endGame 调用栈内同步触发(@Watch 在赋值后立即) // 但 onGameStateChange 里改其他 state 也并入本次批量

关键经验@Watch 回调在「赋值后同步触发」,仍在同一调用栈——回调里改 state 也并入批量。

八、调试技巧

  1. console.info打重渲染次数:在 build() 首行 log,统计调用次数,对比批量效果。
  2. DevEco Profiler 看 Render:统计帧的 build() 调用数,被动批量应只 1 次。
  3. 跨帧排查:检查是否有 setTimeout/await 断调用栈;检查异步后是否 batchUpdate。
  4. @Watch 触发排查:在 onGameStateChange 首行 log,追是否同帧触发。

九、性能与最佳实践

  1. 同步连续改用被动批量——同调用栈自动合并,不用包 batchUpdate。
  2. 异步后连续改用 batchUpdate(V2)——强制合并跨帧改动。
  3. 避免 setTimeout(0) 分次改——跨帧多次重渲染,浪费。
  4. Promise.all 并发减少 await——一次 await 拿所有数据,后续被动批量。
  5. @State 浅观察和 @Observed 深观察共用批量队列——同帧都合并。
  6. @Watch 回调同帧触发——回调里改 state 也并入本次批量。

十、阶段二进度小结(31-45)

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

主题核心要点
31@State响应式状态,改变触发重渲染
32多 @State同帧批量更新,一次重渲染
33setInterval 主循环100ms 物理周期,暂停短路
34计时器1000ms 秒级,formatTime 格式化
35spawnTimer2000ms 自动生成,随机列选择
36clearTimers-1 哨兵,三时机清理,aboutToDisappear 兜底
37onClick 列投放闭包捕获 col,handleColumnClick 三步
38箭头函数 this回调统一箭头保留 this,普通函数打断链
39@Watch状态变化副作用集中,防递归守卫
40@Prop父子单向只读,浅拷贝,显示型子组件
41@Link父子双向,$val 传引用,编辑型子组件
42@Provide/@Consume跨层隐式共享,避免 prop drilling
43@Observed+@ObjectLink类实例深观察,改内部属性触发
44数组项替换ForEach 密钥 diff + 深观察协同
45(本篇)批量更新被动批量(同帧)+ batchUpdate(异步后)

接下来第 46-50 篇会进入 V1 收尾与 V2 迁移:嵌套陷阱、@Reusable 列表复用、LazyForEach 大列表、V1 局限总结,然后 V2 迁移:@Local、@Param、@Event、@ObservedV2+@Trace、@Monitor。

总结

本篇我们从批量状态更新切入,掌握了被动批量(同调用栈自动合并)主动批量(batchUpdate 异步后强制合并)跨帧陷阱(setTimeout/await 断调用栈)批量与深观察协同四大要点,并给出了 endGame 5 个 state 同帧批量的完整代码。核心要点:同步连续改被动批量;异步后用 batchUpdate;避免 setTimeout 分次;Promise.all 减少 await

下一篇我们将拆解嵌套陷阱——@State 嵌套对象/数组的更新坑。

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


相关资源:

  • 「猫猫大作战」项目源码:本仓库entry/src/main/ets/pages/Index.ets
  • ArkUI 状态管理批量更新官方指南
  • ArkUI 状态管理概述
  • ArkUI 渲染管线与性能最佳实践
  • 开源鸿蒙跨平台社区
  • HarmonyOS 开发者官方文档首页
  • 系列索引:本仓库articles/INDEX.md
http://www.jsqmd.com/news/1277377/

相关文章:

  • 银川及周边物流运输实力公司联系方式实用参考指引 - 热点品牌推荐
  • 2026光明三级抗震螺纹钢/房建盘螺厂家选购指南:6个避坑要点+5条硬标准 - mobible
  • 使用Unidbg Debugger逆向分析魔改哈希算法:动态调试实战指南
  • 2026 年 7 月新发布:宝鸡可靠的厂区围墙护栏定制厂家哪家好,厂区围墙护栏居然藏着这么多安全密码,90%的人都没留心到?-京标丝网 - 鉴选官
  • 2026年上海NCM锂电池回收品牌大评测:哪家靠谱?
  • 2026浙江选购电源连接线可参考这些靠谱厂家 - 起跑123
  • 2026年龙门洗车机生产厂家如何选 靠谱选型实用指南 - 热点品牌推荐
  • AI项目从入门到上线21-从需求分析到部署运维全让AI干?AI驱动的DevOps平台架构揭秘
  • 2026年橡胶护舷源头厂家哪家好 实用采购参考指南 - 热点品牌推荐
  • 2026惠州HRB400钢筋厂家推荐:工地钢筋选购避坑指南与源头厂家实用攻略 - mobible
  • HarmonyOS应用开发实战:猫猫大作战-onKeyEvent 三阶段触发、KeyCode 判定具体键、KeyEventSource 判定输入源、与
  • HarmonyOS应用开发实战:猫猫大作战-bindSheet 的使用方式
  • 音频转换成文字在线怎么操作:逐步点哪里 - AI测评专家
  • 2026年提升内容AI收录的视频拍摄服务相关了解分享 - 起跑123
  • 2026东莞装修避坑指南!280亿市场大浪里,挑靠谱装修就看这波口碑榜 - dgqw123456
  • c++14 新增内容
  • 2026 年现阶段,邻水专业的楼板切割制造商格局重塑与选型新思路,装修时悄悄动这处结构,竟能省下十几万?很多人都踩了它的坑-洪春绳锯建筑 - 行业推荐官【官方】
  • 查询阿维菌素肥料优质厂商联系方式 正规靠谱源头农资商家汇总 - 热点品牌推荐
  • HarmonyOS应用开发实战:猫猫大作战-transition 装饰绑定、TransitionType 推/拉/全部、与 if 条件渲染协同、与 H
  • 扒开 HTTP/1.1 的底裤:报文结构、状态码与连接管理一次讲透
  • (2026最新)临沂本地漏水检测维修公司靠谱推荐:正规防水补漏上门维修-墙面/屋顶/外墙/暗管漏水检测精准定位 - 即刻修防水
  • 2026年国内高口碑淋浴房生产厂家实用选购参考指南 - 热点品牌推荐
  • 2026保姆级教程:Excel转PDF免费无水印,WPS/Office/在线工具全方法手把手教学 - 爱上科技热点
  • 2026年陕西西安GEO优化公司对比之沄启智能相关介绍 - 起跑123
  • 非洲黑石材源头工厂对接渠道 正规联系方式实用查询攻略 - 热点品牌推荐
  • 海南本地电视柜造型公司报价明细及采购选型参考 - 热点品牌推荐
  • 2026 北京业主常见疑惑汇总:漏水着急入住能不能加急施工,防水施工需要全天留人吗,北京各区上门时效,各类渗漏维修工期长短,轻微小漏水起步收费标准。 - 伶鹿到家
  • 2026年四川木质套装门制造商推荐几家 高适配性厂商选型参考 - 热点品牌推荐
  • 辽宁性价比高的水泥房源头工厂靠谱选购实用指南 - 热点品牌推荐
  • 告别网盘限速:netdisk-fast-download让你的下载速度飞起来