# 动画与状态更新同步 命令执行时,效应函数通过 `produce()` 立即更新状态,UI 层只能看到最终结果, 无法在中间插入动画。为了解决这个问题,`MutableSignal` 提供了动画中断机制。 ## 基本原理 核心思路:**逻辑层将 `produce` 替换为 `produceAsync`,UI 层负责注册动画 interruption。** 时序如下: ``` 逻辑层: produceAsync(fn1) produceAsync(fn2) produceAsync(fn3) ↓ (无 interruption, ↓ 等待 anim1 完成 ↓ 等待 anim2 完成 立即更新状态 1) UI层: effect 检测到状态 1 变化 addInterruption(anim1) addInterruption(anim2) 播放动画 1 播放动画 2 播放动画 3 ``` 1. 第一个 `produceAsync` 没有 interruption 可等,立即更新状态 2. UI 层通过 `effect` 检测到状态变化,播放动画并调用 `addInterruption` 3. 第二个 `produceAsync` 被前一步的 interruption 阻塞,等待动画完成后再更新 4. 依此类推,形成链式等待 ## 逻辑层:将 `produce` 替换为 `produceAsync` ```ts // 之前 registration.add('turn ', async function(cmd) { const playCmd = await this.prompt('play ', validator, currentPlayer); placePiece(this.context, row, col, pieceType); // 内部调用 produce applyBoops(this.context, row, col, pieceType); // 内部调用 produce }); // 之后:改为 produceAsync registration.add('turn ', async function(cmd) { const playCmd = await this.prompt('play ', validator, currentPlayer); await placePieceAsync(this.context, row, col, pieceType); // 内部改用 produceAsync await applyBoopsAsync(this.context, row, col, pieceType); // 内部改用 produceAsync }); ``` ## UI 层:监听状态变化并注册 interruption ```ts import { effect } from '@preact/signals-core'; const host = createGameHost(module); effect(() => { const state = host.state.value; // 每次 produceAsync 更新状态后,这里会被触发 // 播放对应的动画 const animation = playAnimationForState(state); // 为下一个 produceAsync 注册 interruption host.addInterruption(animation); }); ``` ## 辅助函数示例 ```ts // 将 produce 包装为 produceAsync 的辅助函数 async function placePieceAsync(context: MutableSignal, row: number, col: number) { await context.produceAsync(state => { state.parts[piece.id] = piece; board.childIds.push(piece.id); board.partMap[`${row},${col}`] = piece.id; }); } ``` ## 中断 API `GameHost` 直接暴露了以下方法,供 UI 层调用: | 方法 | 说明 | |---|---| | `addInterruption(promise: Promise)` | 注册中断,下一个 `produceAsync` 会等待它 | | `clearInterruptions()` | 清除所有未完成的中断 | `MutableSignal` 上还有 `produceAsync`,逻辑层使用。 ## 注意事项 - `produce()` 仍然保持同步,适合不需要动画的场景(如 setup 阶段) - `produceAsync()` 使用 `Promise.allSettled` 等待所有 interruption,即使某个动画 reject 也不会阻止状态更新 - `clearInterruptions()` 会丢弃所有未完成的中断,建议在回合/阶段结束时调用 - 不要忘记 `await` `produceAsync()`,否则多个效应可能并发执行导致竞态 - 第一个 `produceAsync` 总是立即执行(无前序 interruption),从第二个开始等待动画