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