前言
上一篇我们用单个@State score驱动 HUD 得分刷新——一加一减,UI 自动更新。但实战中状态很少是孤立的:得分变化往往伴随连击刷新、猫咪数组更新、最高分判断。多个@State在同一帧内一起改变,ArkUI 怎么处理?会不会重渲染多次导致卡顿?
本篇以「猫猫大作战」主循环里多个 @State 协同更新为锚点,把多 @State 同帧批量更新、复合状态依赖、批量更新的性能优势三大要点讲透。
提示:本系列不讲 ArkTS 基础语法与环境搭建,假设你已跟完第 1–31 篇。本篇是阶段二状态管理第二篇。
一、场景拆解:主循环多状态同步
回顾「猫猫大作战」主循环(第 31 篇):
// 来源:entry/src/main/ets/pages/Index.ets startGame() this.gameLoopTimer = setInterval(() => { if (this.gameState !== GameState.PLAYING) return; this.cats = this.gameEngine.updateCats(); // @State cats 改 this.score = this.gameEngine.getScore(); // @State score 改 this.combo = this.gameEngine.getCombo(); // @State combo 改 if (this.gameEngine.isGameOver()) { this.endGame(); } }, 100);核心问题:一次setInterval回调里改了 3 个@State(cats/score/combo),ArkUI 会重渲染 3 次吗?
答案:不会,ArkUI 同帧批量更新——3 个 state 改完,下一个 VSync 只触发一次重渲染。
二、多 @State 协同更新机制
2.1 同帧批量更新
// 同一个回调(同一帧)里改多个 @State this.cats = [...newCats]; // 改 1 this.score = newScore; // 改 2 this.combo = newCombo; // 改 3 // ArkUI 收集所有变更,下一帧只 build() 一次执行流程:
this.cats = [...]→ ArkUI 标记「cats 变了」,不立即重渲染。this.score = 99→ ArkUI 标记「score 变了」,不立即重渲染。this.combo = {...}→ ArkUI 标记「combo 变了」,不立即重渲染。- 当前帧 JS 执行完 → 下一个 VSync →一次性 build() 所有脏组件。
关键经验:ArkUI 的状态更新是「标记 + 批量刷新」——同帧多次改 state 只触发一次重渲染。
2.2 跨帧多次更新
// 不同帧改 state,每次都触发重渲染 setInterval(() => { this.score++; }, 100); // 每 100ms 改一次,每 100ms 重渲染一次对比:
| 更新方式 | 重渲染次数 | 性能 |
|---|---|---|
| 同帧改 3 个 state | 1 次 | ✅ 最优 |
| 跨帧改 3 个 state | 3 次 | ❌ 浪费 |
2.3 复合状态依赖
HUD 同时依赖score、combo、gameTime三个 state:
@Builder GameHUD() { Row() { // 依赖 @State score Column() { Text('得分').fontSize(11).fontColor('#95A5A6') Text(this.score.toString()) .fontSize(22).fontWeight(FontWeight.Bold).fontColor('#2C3E50') } .alignItems(HorizontalAlign.Start) Spacer() // 依赖 @State combo if (this.combo.count > 1) { Row() { Text(`🔥 x${this.combo.multiplier}`) .fontSize(18).fontWeight(FontWeight.Bold).fontColor('#E74C3C') } .padding({ left: 12, right: 12, top: 4, bottom: 4 }) .backgroundColor('rgba(231, 76, 60, 0.1)') .borderRadius(16) } Spacer() // 依赖 @State gameTime Column() { Text('时间').fontSize(11).fontColor('#95A5A6') Text(this.formatTime(this.gameTime)) .fontSize(18).fontWeight(FontWeight.Medium).fontColor('#2C3E50') } .alignItems(HorizontalAlign.End) } .width('100%') .padding({ left: 20, right: 20, top: 12, bottom: 8 }) }依赖关系图:
@State score ──→ 得分栏 Text @State combo ──→ 连击栏 Row + Text @State gameTime ──→ 时间栏 Text三个 state 任一变化,对应栏重渲染。同帧三个都变,整行 HUD 只重渲染一次。
三、主循环多状态同步实战
3.1 完整主循环
// 来源:entry/src/main/ets/pages/Index.ets startGame() { this.clearTimers(); this.gameEngine.reset(); this.gameState = GameState.PLAYING; this.score = 0; this.cats = []; this.gameTime = 0; this.combo = { count: 0, multiplier: 1, lastMergeTime: 0 }; this.nextCatLevel = this.gameEngine.getNextCatLevel(); // 游戏主循环 - 100ms 更新物理 this.gameLoopTimer = setInterval(() => { if (this.gameState !== GameState.PLAYING) return; // 一次回调改 4 个 @State,ArkUI 同帧批量更新 this.cats = this.gameEngine.updateCats(); this.score = this.gameEngine.getScore(); this.combo = this.gameEngine.getCombo(); this.nextCatLevel = this.gameEngine.getNextCatLevel(); if (this.gameEngine.isGameOver()) { this.endGame(); } }, 100); // 自动生成猫咪 this.spawnTimer = setInterval(() => { if (this.gameState !== GameState.PLAYING) return; if (this.gameEngine.getCatCount() < GameConfig.MAX_CATS) { this.gameEngine.autoSpawnCat(); this.cats = this.gameEngine.getAllCats(); this.nextCatLevel = this.gameEngine.getNextCatLevel(); } }, GameConfig.SPAWN_RATE); // 计时器 this.timeTimer = setInterval(() => { if (this.gameState === GameState.PLAYING) { this.gameTime++; } }, 1000); }3.2 三个定时器的协同
| 定时器 | 周期 | 改的 @State |
|---|---|---|
gameLoopTimer | 100ms | cats、score、combo、nextCatLevel |
spawnTimer | 2000ms | cats、nextCatLevel |
timeTimer | 1000ms | gameTime |
协同场景:
- 第 0ms:
gameLoopTimer触发,改 cats/score/combo/nextCatLevel。 - 第 100ms:
gameLoopTimer再触发,同帧批量更新。 - 第 1000ms:
timeTimer触发,改 gameTime;gameLoopTimer也触发,改 4 个 state——同帧 5 个 state 一起改,一次重渲染。 - 第 2000ms:
spawnTimer触发,改 cats/nextCatLevel;gameLoopTimer也触发——同帧批量。
关键经验:多个定时器同帧触发时,所有 state 改动合并成一次重渲染——这是 ArkUI 批量更新的核心优势。
3.3 endGame 的多状态更新
// 来源: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(); }endGame()一次改 5 个 state(gameState、maxCombo、mergeCount、highestLevel、highScore),ArkUI 同帧批量更新,游戏结束弹窗(GameOverOverlay)只重渲染一次。
四、批量更新的性能优势
4.1 重渲染成本对比
假设 HUD 有 3 个Text组件依赖 3 个 state:
| 更新方式 | 重渲染次数 | build() 调用 | 性能 |
|---|---|---|---|
| 逐个改(跨帧) | 3 次 | 3 次 | ❌ |
| 同帧批量改 | 1 次 | 1 次 | ✅ |
关键经验:同帧批量更新比分次更新快 3 倍——build() 是昂贵的,能合并就合并。
4.2 避免逐次 setState
// ❌ 错误:用 setTimeout 分次改,导致 3 次重渲染 setTimeout(() => { this.score = 99; }, 0); setTimeout(() => { this.combo = {...}; }, 0); setTimeout(() => { this.cats = [...]; }, 0); // ✅ 正确:同帧一起改,1 次重渲染 this.score = 99; this.combo = { count: 5, multiplier: 3, lastMergeTime: Date.now() }; this.cats = [...this.cats];4.3 复杂场景:合并连击得分
// 场景:一次合并触发连击,得分+连击+合并数+最高等级都要更新 handleMerge() { // 一次回调里批量改 5 个 state this.score = this.gameEngine.getScore(); this.combo = this.gameEngine.getCombo(); this.mergeCount = this.gameEngine.getMergeCount(); this.highestLevel = this.gameEngine.getHighestLevel(); this.cats = this.gameEngine.getAllCats(); // ArkUI 同帧批量更新,HUD + 弹窗只重渲染一次 }五、多 @State 的依赖追踪
5.1 ArkUI 如何知道哪个组件依赖哪个 state?
@Builder GameHUD() { Text(this.score.toString()) // build() 时,ArkUI 记录「这个 Text 依赖 score」 }机制:
- 首次
build()时,ArkUI 执行this.score.toString(),记录 score 被读取。 - 把「score → 这个 Text」的依赖关系存入依赖图。
- 当
this.score = 99时,查依赖图,标记这个 Text 为「脏」。 - 下一个 VSync 帧,重渲染脏组件。
关键经验:依赖关系在 build() 时动态建立——读哪个 state 就依赖哪个。
5.2 条件渲染的依赖变化
@Builder GameHUD() { if (this.combo.count > 1) { // 读 combo,依赖 combo Row() { Text(`🔥 x${this.combo.multiplier}`) // 又读 combo } } }依赖变化:
combo.count = 0→ if 为 false,不渲染连击栏,不依赖 combo。combo.count = 5→ if 为 true,渲染连击栏,开始依赖 combo。
关键经验:条件渲染会动态增减依赖——ArkUI 在每次 build() 时重新计算依赖图。
六、踩坑提示
6.1 跨帧分次改 state
// ❌ 错误:分次改,3 次重渲染 this.score = 99; await sleep(0); // 让出当前帧 this.combo = {...}; await sleep(0); this.cats = [...]; // ✅ 正确:同帧一起改 this.score = 99; this.combo = {...}; this.cats = [...];6.2 闭包里改 state 丢 this
// ❌ 错误:普通函数 this 不指向组件 setInterval(function () { this.score++; this.combo = {...}; }, 100); // ✅ 正确:箭头函数保留外层 this setInterval(() => { this.score++; this.combo = {...}; }, 100);6.3 对象/数组内部属性改了不触发
// ❌ 错误:改 combo.count,不触发重渲染 this.combo.count = 5; // ✅ 正确:重新赋值整个对象 this.combo = { count: 5, multiplier: 3, lastMergeTime: Date.now() };七、调试技巧
console.info打所有 state:主循环里 logscore/combo/cats.length,追同步情况。- DevEco ArkUI Inspector:查看组件依赖的 state,定位重渲染源。
- 重渲染过多排查:检查是否有跨帧分次改 state;检查 build() 内是否改 state。
- 状态不同步排查:检查对象/数组是否重新赋值(而非改内部属性)。
八、性能与最佳实践
- 同帧批量改多个 state——ArkUI 合并成一次重渲染,比分次快 3 倍。
- 避免跨帧分次改 state——用 setTimeout(0) 分次会导致多次重渲染。
- 对象/数组重新赋值——改内部属性不触发重渲染。
- 条件渲染动态增减依赖——ArkUI 在每次 build() 重新计算依赖图。
- 闭包用箭头函数保留 this——setInterval 回调里改 state。
总结
本篇我们从多 @State 协同更新切入,掌握了同帧批量更新机制、复合状态依赖追踪、**批量更新的性能优势(3 倍提速)**三大要点,并给出了主循环多状态同步、endGame 多状态更新的完整代码。核心要点:同帧批量改 state 只重渲染一次;条件渲染动态增减依赖;对象/数组重新赋值才触发。
下一篇我们将拆解 setInterval 主循环——定时器驱动游戏物理。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
- 「猫猫大作战」项目源码:本仓库
entry/src/main/ets/pages/Index.ets - ArkUI 状态管理 @State 官方指南
- ArkUI 状态管理观察机制
- ArkUI 渲染管线最佳实践
- 开源鸿蒙跨平台社区
- HarmonyOS 开发者官方文档首页
- 系列索引:本仓库
articles/INDEX.md