3.5 KiB
3.5 KiB
动画与状态更新同步
命令执行时,效应函数通过 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
- 第一个
produceAsync没有 interruption 可等,立即更新状态 - UI 层通过
effect检测到状态变化,播放动画并调用addInterruption - 第二个
produceAsync被前一步的 interruption 阻塞,等待动画完成后再更新 - 依此类推,形成链式等待
逻辑层:将 produce 替换为 produceAsync
// 之前
registration.add('turn <player>', async function(cmd) {
const playCmd = await this.prompt('play <player> <row:number> <col:number>', validator, currentPlayer);
placePiece(this.context, row, col, pieceType); // 内部调用 produce
applyBoops(this.context, row, col, pieceType); // 内部调用 produce
});
// 之后:改为 produceAsync
registration.add('turn <player>', async function(cmd) {
const playCmd = await this.prompt('play <player> <row:number> <col:number>', validator, currentPlayer);
await placePieceAsync(this.context, row, col, pieceType); // 内部改用 produceAsync
await applyBoopsAsync(this.context, row, col, pieceType); // 内部改用 produceAsync
});
UI 层:监听状态变化并注册 interruption
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);
});
辅助函数示例
// 将 produce 包装为 produceAsync 的辅助函数
async function placePieceAsync(context: MutableSignal<GameState>, 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<void>) |
注册中断,下一个 produceAsync 会等待它 |
clearInterruptions() |
清除所有未完成的中断 |
MutableSignal 上还有 produceAsync,逻辑层使用。
注意事项
produce()仍然保持同步,适合不需要动画的场景(如 setup 阶段)produceAsync()使用Promise.allSettled等待所有 interruption,即使某个动画 reject 也不会阻止状态更新clearInterruptions()会丢弃所有未完成的中断,建议在回合/阶段结束时调用- 不要忘记
awaitproduceAsync(),否则多个效应可能并发执行导致竞态 - 第一个
produceAsync总是立即执行(无前序 interruption),从第二个开始等待动画