跳到主要内容

状态模式

问题

什么是状态模式?如何用状态模式管理复杂的状态转换逻辑?有限状态机(FSM)在前端有哪些典型应用?XState 如何与 React 结合?

答案

状态模式(State Pattern)是 GoF 23 种设计模式中的行为型模式。它允许对象在内部状态改变时改变自身的行为,使对象看起来像是修改了它的类。核心思想是:将每种状态的行为封装到独立的状态类中,通过状态对象的切换来改变整体行为,从而消除大量的条件判断语句

一句话理解

状态模式 = 把 "if 当前状态是 A,就做 X" 这类条件逻辑,变成 "让状态 A 对象自己决定做 X"。状态对象既封装了行为,又负责驱动下一次状态转换。


核心概念与角色

角色说明
Context(上下文)持有当前状态的引用,将行为委托给状态对象。对外暴露统一接口,内部状态对客户端透明
State(抽象状态)定义所有具体状态必须实现的接口,声明状态相关的行为方法
ConcreteState(具体状态)实现特定状态下的行为逻辑,并在满足条件时触发状态转换(调用 context.setState()
状态模式解决的核心问题
  1. 消除臃肿的条件判断 — 用多态取代 if-else / switch-case
  2. 状态转换逻辑内聚 — 每个状态自己管理 "下一步去哪"
  3. 符合开闭原则 — 新增状态只需添加新类,不改已有代码
  4. 避免非法状态转换 — 每个状态只允许合法的跳转

TypeScript 完整实现:自动售货机

以自动售货机为例,包含四种状态:待投币、已投币、出货中、售罄。

vending-machine/state.ts
// 抽象状态接口
interface VendingMachineState {
insertCoin(machine: VendingMachine): void;
ejectCoin(machine: VendingMachine): void;
pressButton(machine: VendingMachine): void;
dispense(machine: VendingMachine): void;
}

// 具体状态:待投币
class NoCoinState implements VendingMachineState {
insertCoin(machine: VendingMachine): void {
console.log('已投币');
machine.setState(machine.getHasCoinState());
}

ejectCoin(_machine: VendingMachine): void {
console.log('请先投币');
}

pressButton(_machine: VendingMachine): void {
console.log('请先投币');
}

dispense(_machine: VendingMachine): void {
console.log('请先投币');
}
}

// 具体状态:已投币
class HasCoinState implements VendingMachineState {
insertCoin(_machine: VendingMachine): void {
console.log('已经投过币了,请勿重复投币');
}

ejectCoin(machine: VendingMachine): void {
console.log('退币成功');
machine.setState(machine.getNoCoinState());
}

pressButton(machine: VendingMachine): void {
console.log('正在出货...');
machine.setState(machine.getSellingState());
// 状态转换后,由新状态执行出货
machine.dispense();
}

dispense(_machine: VendingMachine): void {
console.log('请先按下购买按钮');
}
}

// 具体状态:出货中
class SellingState implements VendingMachineState {
insertCoin(_machine: VendingMachine): void {
console.log('正在出货,请稍候');
}

ejectCoin(_machine: VendingMachine): void {
console.log('正在出货,无法退币');
}

pressButton(_machine: VendingMachine): void {
console.log('正在出货,请勿重复按键');
}

dispense(machine: VendingMachine): void {
machine.releaseProduct();
if (machine.getCount() > 0) {
machine.setState(machine.getNoCoinState());
} else {
console.log('商品已售罄');
machine.setState(machine.getSoldOutState());
}
}
}

// 具体状态:售罄
class SoldOutState implements VendingMachineState {
insertCoin(_machine: VendingMachine): void {
console.log('商品已售罄,无法投币');
}

ejectCoin(_machine: VendingMachine): void {
console.log('未投币,无法退币');
}

pressButton(_machine: VendingMachine): void {
console.log('商品已售罄');
}

dispense(_machine: VendingMachine): void {
console.log('商品已售罄');
}
}
vending-machine/context.ts
// 上下文:售货机
class VendingMachine {
private currentState: VendingMachineState;
private count: number;

// 持有所有状态实例(避免频繁创建)
private noCoinState: VendingMachineState;
private hasCoinState: VendingMachineState;
private sellingState: VendingMachineState;
private soldOutState: VendingMachineState;

constructor(count: number) {
this.noCoinState = new NoCoinState();
this.hasCoinState = new HasCoinState();
this.sellingState = new SellingState();
this.soldOutState = new SoldOutState();
this.count = count;
this.currentState = count > 0 ? this.noCoinState : this.soldOutState;
}

// 委托给当前状态
insertCoin(): void { this.currentState.insertCoin(this); }
ejectCoin(): void { this.currentState.ejectCoin(this); }
pressButton(): void { this.currentState.pressButton(this); }
dispense(): void { this.currentState.dispense(this); }

releaseProduct(): void {
if (this.count > 0) {
this.count--;
console.log(`出货成功!剩余库存: ${this.count}`);
}
}

setState(state: VendingMachineState): void { this.currentState = state; }
getCount(): number { return this.count; }
getNoCoinState(): VendingMachineState { return this.noCoinState; }
getHasCoinState(): VendingMachineState { return this.hasCoinState; }
getSellingState(): VendingMachineState { return this.sellingState; }
getSoldOutState(): VendingMachineState { return this.soldOutState; }
}

// 使用
const machine = new VendingMachine(2);
machine.insertCoin(); // 已投币
machine.pressButton(); // 正在出货... → 出货成功!剩余库存: 1
machine.insertCoin(); // 已投币
machine.pressButton(); // 正在出货... → 出货成功!剩余库存: 0 → 商品已售罄
machine.insertCoin(); // 商品已售罄,无法投币

有限状态机(FSM)

有限状态机(Finite State Machine)是一个数学计算模型,由以下五元组定义:

要素说明示例
States有限数量的状态集合idle, loading, success, error
Events触发状态转换的事件FETCH, RESOLVE, REJECT, RETRY
Transitions状态 + 事件 → 新状态 的映射关系idle + FETCH → loading
Initial State初始状态idle
Actions状态转换时执行的副作用发请求、显示通知
确定性 vs 非确定性

前端状态机几乎都是确定性状态机(DFA):同一状态下,相同事件只会导致唯一确定的转换结果。这保证了行为的可预测性。

状态转换表

转换表来形式化定义状态机,比代码更清晰:

当前状态事件下一状态动作
idleFETCHloading发起请求
loadingRESOLVEsuccess存储数据
loadingREJECTerror存储错误信息
errorRETRYloading重新发起请求
successREFRESHloading刷新数据

对应 TypeScript 实现:

fsm/simple-fsm.ts
// 泛型状态机
type TransitionMap<S extends string, E extends string> = {
[state in S]?: {
[event in E]?: {
target: S;
action?: () => void;
};
};
};

class StateMachine<S extends string, E extends string> {
private current: S;
private transitions: TransitionMap<S, E>;

constructor(initial: S, transitions: TransitionMap<S, E>) {
this.current = initial;
this.transitions = transitions;
}

send(event: E): void {
const stateTransitions = this.transitions[this.current];
const transition = stateTransitions?.[event];

if (!transition) {
console.warn(`无法从 "${this.current}" 状态处理 "${event}" 事件`);
return;
}

console.log(`${this.current} --[${event}]--> ${transition.target}`);
transition.action?.();
this.current = transition.target;
}

getState(): S {
return this.current;
}
}

// 使用示例:请求状态机
type RequestState = 'idle' | 'loading' | 'success' | 'error';
type RequestEvent = 'FETCH' | 'RESOLVE' | 'REJECT' | 'RETRY' | 'REFRESH';

const requestMachine = new StateMachine<RequestState, RequestEvent>(
'idle',
{
idle: {
FETCH: { target: 'loading', action: () => console.log('开始请求') },
},
loading: {
RESOLVE: { target: 'success', action: () => console.log('请求成功') },
REJECT: { target: 'error', action: () => console.log('请求失败') },
},
success: {
REFRESH: { target: 'loading', action: () => console.log('刷新数据') },
},
error: {
RETRY: { target: 'loading', action: () => console.log('重试请求') },
},
}
);

requestMachine.send('FETCH'); // idle --[FETCH]--> loading
requestMachine.send('RESOLVE'); // loading --[RESOLVE]--> success
requestMachine.send('FETCH'); // 警告:无法从 "success" 处理 "FETCH"
requestMachine.send('REFRESH'); // success --[REFRESH]--> loading

前端典型状态机场景

1. 请求状态机

这是前端最常见的状态机场景。

hooks/useRequestMachine.ts
type RequestState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: unknown }
| { status: 'error'; error: Error };

type RequestEvent =
| { type: 'FETCH' }
| { type: 'RESOLVE'; data: unknown }
| { type: 'REJECT'; error: Error }
| { type: 'RETRY' }
| { type: 'RESET' };

function requestReducer(state: RequestState, event: RequestEvent): RequestState {
switch (state.status) {
case 'idle':
if (event.type === 'FETCH') return { status: 'loading' };
return state;

case 'loading':
if (event.type === 'RESOLVE') return { status: 'success', data: event.data };
if (event.type === 'REJECT') return { status: 'error', error: event.error };
return state;

case 'success':
if (event.type === 'FETCH') return { status: 'loading' };
return state;

case 'error':
if (event.type === 'RETRY') return { status: 'loading' };
if (event.type === 'RESET') return { status: 'idle' };
return state;

default:
return state;
}
}
关键优势

这种 reducer 写法的核心优势在于:每个状态只响应合法的事件。你永远不会在 idle 状态下处理 RESOLVE 事件,也不会在 success 状态下处理 REJECT 事件。这消除了 "不可能状态" 带来的 Bug。

2. 表单状态机

hooks/useFormMachine.ts
type FormStatus = 'pristine' | 'dirty' | 'validating' | 'submitting' | 'submitted';

interface FormState {
status: FormStatus;
values: Record<string, unknown>;
errors: Record<string, string>;
submitCount: number;
}

type FormEvent =
| { type: 'CHANGE'; field: string; value: unknown }
| { type: 'SUBMIT' }
| { type: 'VALID' }
| { type: 'INVALID'; errors: Record<string, string> }
| { type: 'SUCCESS' }
| { type: 'FAILURE'; errors: Record<string, string> }
| { type: 'RESET' };

function formReducer(state: FormState, event: FormEvent): FormState {
switch (state.status) {
case 'pristine':
if (event.type === 'CHANGE') {
return {
...state,
status: 'dirty',
values: { ...state.values, [event.field]: event.value },
};
}
return state;

case 'dirty':
if (event.type === 'CHANGE') {
return {
...state,
values: { ...state.values, [event.field]: event.value },
};
}
if (event.type === 'SUBMIT') {
return { ...state, status: 'validating' };
}
return state;

case 'validating':
if (event.type === 'VALID') {
return { ...state, status: 'submitting', errors: {} };
}
if (event.type === 'INVALID') {
return { ...state, status: 'dirty', errors: event.errors };
}
return state;

case 'submitting':
if (event.type === 'SUCCESS') {
return {
...state,
status: 'submitted',
submitCount: state.submitCount + 1,
};
}
if (event.type === 'FAILURE') {
return { ...state, status: 'dirty', errors: event.errors };
}
return state;

case 'submitted':
if (event.type === 'RESET') {
return { status: 'pristine', values: {}, errors: {}, submitCount: state.submitCount };
}
return state;

default:
return state;
}
}

3. 播放器控制状态机

player/player-machine.ts
type PlayerState = 'stopped' | 'playing' | 'paused';
type PlayerEvent = 'PLAY' | 'PAUSE' | 'STOP';

const playerTransitions: Record<PlayerState, Partial<Record<PlayerEvent, PlayerState>>> = {
stopped: { PLAY: 'playing' },
playing: { PAUSE: 'paused', STOP: 'stopped' },
paused: { PLAY: 'playing', STOP: 'stopped' },
};

function playerReducer(state: PlayerState, event: PlayerEvent): PlayerState {
return playerTransitions[state][event] ?? state;
}

4. 购物流程状态机

shopping/shopping-flow.ts
type ShoppingState = 'browsing' | 'cart' | 'checkout' | 'payment' | 'confirmed';

type ShoppingEvent =
| 'ADD_TO_CART'
| 'CONTINUE_SHOPPING'
| 'CHECKOUT'
| 'BACK_TO_CART'
| 'CONFIRM_ORDER'
| 'CHANGE_PAYMENT'
| 'PAY_SUCCESS'
| 'PAY_FAILED'
| 'NEW_ORDER';

const shoppingTransitions: Record<ShoppingState, Partial<Record<ShoppingEvent, ShoppingState>>> = {
browsing: { ADD_TO_CART: 'cart' },
cart: { CONTINUE_SHOPPING: 'browsing', CHECKOUT: 'checkout' },
checkout: { BACK_TO_CART: 'cart', CONFIRM_ORDER: 'payment' },
payment: { CHANGE_PAYMENT: 'checkout', PAY_SUCCESS: 'confirmed', PAY_FAILED: 'payment' },
confirmed: { NEW_ORDER: 'browsing' },
};

XState 状态机库

XState 是前端最流行的状态机/状态图库,提供了强类型可视化Actor 模型等能力。

基础用法

machines/fetchMachine.ts
import { setup, assign } from 'xstate';

const fetchMachine = setup({
types: {
context: {} as {
data: unknown | null;
error: string | null;
retries: number;
},
events: {} as
| { type: 'FETCH'; url: string }
| { type: 'RESOLVE'; data: unknown }
| { type: 'REJECT'; error: string }
| { type: 'RETRY' },
},
}).createMachine({
id: 'fetch',
initial: 'idle',
context: {
data: null,
error: null,
retries: 0,
},
states: {
idle: {
on: {
FETCH: { target: 'loading' },
},
},
loading: {
on: {
RESOLVE: {
target: 'success',
actions: assign({
data: ({ event }) => event.data,
error: () => null,
}),
},
REJECT: {
target: 'error',
actions: assign({
error: ({ event }) => event.error,
}),
},
},
},
success: {
on: {
FETCH: { target: 'loading' },
},
},
error: {
on: {
RETRY: {
target: 'loading',
guard: ({ context }) => context.retries < 3,
actions: assign({
retries: ({ context }) => context.retries + 1,
}),
},
},
},
},
});

与 React 结合

components/DataFetcher.tsx
import { useMachine } from '@xstate/react';
import { fetchMachine } from '../machines/fetchMachine';

function DataFetcher({ url }: { url: string }) {
const [state, send] = useMachine(fetchMachine);

const handleFetch = async () => {
send({ type: 'FETCH', url });
try {
const response = await fetch(url);
const data = await response.json();
send({ type: 'RESOLVE', data });
} catch (err) {
send({ type: 'REJECT', error: (err as Error).message });
}
};

return (
<div>
{state.matches('idle') && (
<button onClick={handleFetch}>加载数据</button>
)}
{state.matches('loading') && <p>加载中...</p>}
{state.matches('success') && (
<pre>{JSON.stringify(state.context.data, null, 2)}</pre>
)}
{state.matches('error') && (
<div>
<p>错误: {state.context.error}</p>
<button onClick={() => send({ type: 'RETRY' })}>
重试 ({state.context.retries}/3)
</button>
</div>
)}
</div>
);
}
XState 可视化工具

XState 提供了在线可视化编辑器 Stately Editor,可以直接拖拽生成状态机代码,也可以将代码粘贴进去查看状态图,非常适合团队协作和文档沟通。

XState 核心特性

特性说明
层次状态状态可以嵌套(父子状态),子状态继承父状态的转换
并行状态多个正交状态可以同时活跃
Guard(守卫)条件判断,满足条件才允许转换
Actions(动作)进入/退出状态或转换时执行的副作用
Invoke(调用)调用异步服务(Promise、Observable、其他 Machine)
Actor 模型XState v5 基于 Actor 模型,每个 Machine 都是一个 Actor

状态模式 vs 策略模式

状态模式和策略模式在结构上非常相似(都是通过多态委托来替代条件判断),但它们的意图和使用方式有本质区别。

对比维度状态模式策略模式
核心意图管理对象的状态转换和行为变化封装可互换的算法
谁触发切换状态对象自动触发下一次转换客户端从外部选择策略
状态对象是否知道彼此是,状态 A 知道可以转换到状态 B否,策略之间完全独立
转换是否有约束是,只允许合法的状态转换路径否,任意策略可自由切换
Context 感知状态对象持有 Context 引用,可修改上下文策略通常只接收参数,不修改上下文
典型场景订单流程、播放器、TCP 连接排序算法、支付方式、折扣计算
对比示例
// 策略模式 — 外部选择,策略之间无关联
const strategy = strategies[userChoice]; // 用户选哪个就用哪个
strategy.execute(data);

// 状态模式 — 内部自动转换,有固定的转换路径
class LoadingState implements State {
handle(context: Context): void {
// 状态自己决定下一步去哪
if (success) {
context.setState(new SuccessState()); // 自动转换
} else {
context.setState(new ErrorState()); // 自动转换
}
}
}
常见误区

面试中经常有人将两者混淆。记住核心区别:策略模式是 "你来选"(外部驱动),状态模式是 "我自己转"(内部驱动)。如果对象行为的变化取决于用户的选择,用策略模式;如果取决于对象内部条件的变化,用状态模式。


状态模式 vs if-else/switch

为什么要用状态模式替代大量的条件判断?

bad-example.ts
// ❌ if-else 噩梦 — 难以维护
class Order {
status: string = 'pending';

process(): void {
if (this.status === 'pending') {
console.log('审核订单');
this.status = 'approved';
} else if (this.status === 'approved') {
console.log('开始发货');
this.status = 'shipping';
} else if (this.status === 'shipping') {
console.log('确认收货');
this.status = 'delivered';
} else if (this.status === 'delivered') {
console.log('订单完成');
this.status = 'completed';
}
// 新增状态?继续加 else if...
// cancel() 方法也要写一遍同样的条件判断...
}

cancel(): void {
if (this.status === 'pending') {
console.log('取消订单');
this.status = 'cancelled';
} else if (this.status === 'approved') {
console.log('取消并退款');
this.status = 'cancelled';
} else if (this.status === 'shipping') {
console.log('拒收并退款');
this.status = 'cancelled';
} else {
console.log('无法取消');
}
}
}
对比维度if-else / switch状态模式
新增状态修改所有方法中的条件判断只需添加新的状态类
新增行为修改每个条件分支在 State 接口中添加方法
非法状态防护需要手动检查不存在该事件的处理方法则自动忽略
可测试性每个方法都要覆盖所有状态分支每个状态类独立测试
代码组织按行为聚合(一个方法里判断所有状态)按状态聚合(一个类里包含该状态的所有行为)
何时使用状态模式
  • 状态数量 >= 3 且转换逻辑复杂
  • 多个方法都需要根据状态执行不同行为
  • 状态之间有明确的转换约束

如果只有 2-3 个简单状态且行为单一,直接用 if-else 反而更简洁。


函数式状态机(轻量方案)

在大多数前端场景中,不需要完整的 OOP 状态模式,可以用更轻量的函数式方案:

fsm/functional-state-machine.ts
// 类型安全的函数式状态机
type MachineConfig<S extends string, E extends string> = {
initial: S;
states: {
[K in S]: {
on?: Partial<Record<E, S>>;
entry?: () => void;
exit?: () => void;
};
};
};

function createMachine<S extends string, E extends string>(config: MachineConfig<S, E>) {
let current: S = config.initial;
config.states[current].entry?.();

return {
getState: (): S => current,

send: (event: E): S => {
const stateConfig = config.states[current];
const nextState = stateConfig.on?.[event];

if (nextState && nextState !== current) {
stateConfig.exit?.();
current = nextState;
config.states[current].entry?.();
}

return current;
},

matches: (state: S): boolean => current === state,
};
}

// 使用
const toggleMachine = createMachine({
initial: 'inactive' as const,
states: {
inactive: {
on: { TOGGLE: 'active' as const },
entry: () => console.log('已关闭'),
},
active: {
on: { TOGGLE: 'inactive' as const },
entry: () => console.log('已激活'),
},
},
});

toggleMachine.send('TOGGLE'); // "已激活"
toggleMachine.send('TOGGLE'); // "已关闭"

React Hook 封装

hooks/useMachine.ts
import { useState, useCallback, useRef } from 'react';

function useMachine<S extends string, E extends string>(
config: MachineConfig<S, E>
) {
const [state, setState] = useState<S>(config.initial);
const stateRef = useRef(state);
stateRef.current = state;

const send = useCallback((event: E) => {
const currentState = stateRef.current;
const stateConfig = config.states[currentState];
const nextState = stateConfig.on?.[event];

if (nextState && nextState !== currentState) {
stateConfig.exit?.();
setState(nextState);
config.states[nextState].entry?.();
}
}, [config]);

const matches = useCallback(
(s: S) => stateRef.current === s,
[]
);

return { state, send, matches };
}

// 在组件中使用
function ToggleButton() {
const { state, send } = useMachine({
initial: 'off' as const,
states: {
off: { on: { TOGGLE: 'on' as const } },
on: { on: { TOGGLE: 'off' as const } },
},
});

return (
<button onClick={() => send('TOGGLE')}>
当前状态: {state}
</button>
);
}

常见面试问题

Q1: 状态模式的优缺点分别是什么?

答案

优点缺点
消除大量 if-else / switch 条件判断状态类数量增多,代码量增大
符合开闭原则,新增状态只需添加新类状态转换逻辑分散在各个状态类中,不如转换表直观
每个状态的行为内聚,职责清晰简单场景下过度设计
状态转换有约束,不会出现非法状态状态类之间可能存在耦合

Q2: 什么是有限状态机?它在前端有哪些应用场景?

答案

有限状态机(FSM)是一种数学计算模型,由有限个状态、事件、转换规则和初始状态组成。在任意时刻,系统只能处于一个确定的状态,接收到事件后按照预定义的规则转换到下一个状态。

前端常见应用场景:

  1. 数据请求idle → loading → success / error,防止在 loading 时重复发请求
  2. 表单状态pristine → dirty → validating → submitting → submitted
  3. UI 组件:弹窗(closed → opening → open → closing)、下拉菜单、Toast
  4. 业务流程:购物车→结算→支付→完成、审批流程
  5. 游戏逻辑:角色状态(idle → walking → jumping → attacking)
  6. 连接管理:WebSocket 状态(disconnected → connecting → connected → reconnecting)
  7. 动画控制:多步骤动画的状态管理

Q3: 状态模式和策略模式有什么区别?

答案

两者在结构上几乎一致(Context + Strategy/State 接口 + 具体实现),但意图不同:

  • 策略模式:客户端主动选择使用哪个算法,策略之间互不知晓,可任意替换
  • 状态模式:状态对象自动转换到下一个状态,状态之间存在预定义的转换路径
// 策略模式:外部驱动
const sorter = strategies[userSelected]; // 用户决定
sorter.sort(data);

// 状态模式:内部驱动
class PlayingState implements PlayerState {
pause(player: Player): void {
player.setState(new PausedState()); // 状态自己决定
}
}

核心口诀:策略是 "你来选",状态是 "我自己转"。更多关于策略模式的内容可以参考策略模式文档。

Q4: 如何用 useReducer 实现一个类型安全的状态机?

答案

关键技巧是先判断当前状态,再判断事件类型,这样 TypeScript 可以自动推断出合法的转换:

hooks/useStateMachine.ts
import { useReducer } from 'react';

// 可辨识联合定义状态
type FetchState =
| { status: 'idle' }
| { status: 'loading'; startTime: number }
| { status: 'success'; data: string[] }
| { status: 'error'; error: string; retries: number };

type FetchEvent =
| { type: 'FETCH' }
| { type: 'SUCCESS'; data: string[] }
| { type: 'FAILURE'; error: string }
| { type: 'RETRY' }
| { type: 'RESET' };

function fetchReducer(state: FetchState, event: FetchEvent): FetchState {
switch (state.status) {
case 'idle':
if (event.type === 'FETCH')
return { status: 'loading', startTime: Date.now() };
return state;

case 'loading':
if (event.type === 'SUCCESS')
return { status: 'success', data: event.data };
if (event.type === 'FAILURE')
return { status: 'error', error: event.error, retries: 0 };
return state;

case 'success':
if (event.type === 'FETCH')
return { status: 'loading', startTime: Date.now() };
if (event.type === 'RESET')
return { status: 'idle' };
return state;

case 'error':
if (event.type === 'RETRY')
return { status: 'loading', startTime: Date.now() };
if (event.type === 'RESET')
return { status: 'idle' };
return state;
}
}

// 组件中使用
function UserList() {
const [state, dispatch] = useReducer(fetchReducer, { status: 'idle' });

// TypeScript 自动推断 state.data 仅在 success 状态下可用
if (state.status === 'success') {
return <ul>{state.data.map(item => <li key={item}>{item}</li>)}</ul>;
}

if (state.status === 'error') {
return <p>{state.error}(已重试 {state.retries} 次)</p>;
}

return <button onClick={() => dispatch({ type: 'FETCH' })}>加载</button>;
}

Q5: XState 的核心优势是什么?什么时候该用 XState?

答案

核心优势

  1. 形式化建模:基于状态图(Statecharts)理论,支持层次状态、并行状态、Guard 条件
  2. TypeScript 友好setup() API 提供完整类型推断
  3. 可视化调试Stately EditorXState Inspector 可视化状态转换过程
  4. Actor 模型:v5 基于 Actor 模型,支持多状态机通信和组合
  5. 框架无关:可与 React、Vue、Svelte 等任何框架搭配使用

适用场景

场景是否推荐 XState
简单 toggle(开关)不推荐,useState 即可
请求状态(idle/loading/success/error)中等复杂度可考虑,简单的用 useReducer
多步骤表单/向导推荐
复杂业务流程(审批、订单、支付)强烈推荐
拖拽交互、动画状态推荐
需要可视化文档的状态逻辑强烈推荐
选型建议
  • 2-3 个状态 → useState + 条件判断
  • 4-6 个状态且转换简单 → useReducer + 状态机 reducer
  • 复杂状态图(嵌套/并行/Guard/异步) → XState

Q6: 如何防止 "不可能状态"(Impossible States)?

答案

"不可能状态" 是指用多个独立的布尔值管理状态时,产生的逻辑上不应该存在的组合:

impossible-states.ts
// ❌ 不可能状态:isLoading 和 isError 同时为 true
interface BadState {
isLoading: boolean;
isError: boolean;
isSuccess: boolean;
data: string[] | null;
error: Error | null;
}

// 2^3 = 8 种布尔组合,其中很多是非法的
// isLoading: true, isError: true, isSuccess: true ← 不可能!
// isLoading: false, isError: false, isSuccess: false, data: ['...'] ← 也是非法的

// ✅ 用可辨识联合消除不可能状态
type GoodState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: string[] }
| { status: 'error'; error: Error };

// 每个状态只携带该状态需要的数据
// 不可能同时处于 loading 和 error 状态
// data 只在 success 时存在,error 只在 error 时存在

核心原则:用枚举(或字面量联合类型)替代多个布尔值,让类型系统帮你排除非法状态组合。

Q7: 状态模式中,状态转换逻辑应该放在 Context 还是 State 中?

答案

两种方案各有优劣:

方案一:转换逻辑放在 State 中(经典状态模式)

class LoadingState implements State {
handle(context: Context): void {
if (success) context.setState(new SuccessState());
else context.setState(new ErrorState());
}
}
  • 优点:每个状态自治,符合单一职责
  • 缺点:状态之间产生耦合(A 需要知道 B 的存在)

方案二:转换逻辑集中在转换表(FSM 方式)

const transitions = {
loading: { RESOLVE: 'success', REJECT: 'error' },
error: { RETRY: 'loading' },
};
  • 优点:所有转换关系一目了然,便于维护和可视化
  • 缺点:动作逻辑需要额外定义

实践建议:前端开发中推荐使用转换表方案(或 XState),因为它更声明式、更容易维护和测试。传统 OOP 状态模式更适合后端或游戏开发中状态行为非常复杂的场景。

Q8: 请用状态机的思路实现一个 Promise

答案

Promise 本质上就是一个三状态的有限状态机:

promise-as-fsm.ts
type PromiseState = 'pending' | 'fulfilled' | 'rejected';

class SimplePromise<T> {
private state: PromiseState = 'pending';
private value: T | undefined;
private reason: unknown;
private onFulfilledCallbacks: Array<(value: T) => void> = [];
private onRejectedCallbacks: Array<(reason: unknown) => void> = [];

constructor(executor: (resolve: (value: T) => void, reject: (reason: unknown) => void) => void) {
const resolve = (value: T): void => {
// 状态机核心:只有 pending 状态才能转换
if (this.state !== 'pending') return;
this.state = 'fulfilled';
this.value = value;
this.onFulfilledCallbacks.forEach(fn => fn(value));
};

const reject = (reason: unknown): void => {
if (this.state !== 'pending') return;
this.state = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach(fn => fn(reason));
};

try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}

then(onFulfilled?: (value: T) => void, onRejected?: (reason: unknown) => void): void {
if (this.state === 'fulfilled' && onFulfilled) {
onFulfilled(this.value!);
} else if (this.state === 'rejected' && onRejected) {
onRejected(this.reason);
} else {
if (onFulfilled) this.onFulfilledCallbacks.push(onFulfilled);
if (onRejected) this.onRejectedCallbacks.push(onRejected);
}
}
}

Promise 的状态转换规则完美体现了 FSM 的特征:

  • 单向转换pending → fulfilledpending → rejected,不可逆
  • 只转换一次:一旦离开 pending,状态就固定了
  • 确定性:相同的输入(resolve/reject)在 pending 状态下总是产生相同的结果

Q9: 如何在大型项目中组织多个状态机的协作?

答案

大型项目中经常需要多个状态机协作,有几种常见模式:

1. 层次状态机(Hierarchical)

// XState 嵌套状态
const orderMachine = setup({/* ... */}).createMachine({
id: 'order',
initial: 'draft',
states: {
draft: { /* ... */ },
// 嵌套状态机:支付流程是订单的子状态
payment: {
initial: 'selecting',
states: {
selecting: { on: { SELECT: 'processing' } },
processing: { on: { SUCCESS: 'done', FAIL: 'selecting' } },
done: { type: 'final' },
},
// 子状态机完成后,触发父状态转换
onDone: 'shipped',
},
shipped: { /* ... */ },
},
});

2. Actor 模型(XState v5)

// 父状态机 spawn 子 Actor
const parentMachine = setup({/* ... */}).createMachine({
context: {
paymentRef: undefined,
},
states: {
active: {
entry: assign({
paymentRef: ({ spawn }) => spawn(paymentMachine),
}),
},
},
});

3. 事件总线协作

// 多个独立状态机通过事件总线通信
const eventBus = new EventEmitter();

const cartMachine = createMachine(/* ... */);
const authMachine = createMachine(/* ... */);

// authMachine 登出时通知 cartMachine 清空
eventBus.on('AUTH_LOGOUT', () => {
cartMachine.send('CLEAR');
});

Q10: 状态机模式的测试策略是什么?

答案

状态机天然适合测试,因为它是确定性的:给定初始状态和事件序列,最终状态是确定的。

__tests__/fetchMachine.test.ts
import { describe, it, expect } from 'vitest';

describe('请求状态机', () => {
// 1. 测试合法转换
it('idle -> FETCH -> loading', () => {
const state = fetchReducer({ status: 'idle' }, { type: 'FETCH' });
expect(state.status).toBe('loading');
});

// 2. 测试非法转换被忽略
it('idle 状态下 RESOLVE 事件不应生效', () => {
const state = fetchReducer(
{ status: 'idle' },
{ type: 'SUCCESS', data: [] }
);
expect(state.status).toBe('idle'); // 状态不变
});

// 3. 测试完整流程
it('完整的请求成功流程', () => {
let state: FetchState = { status: 'idle' };
state = fetchReducer(state, { type: 'FETCH' });
expect(state.status).toBe('loading');

state = fetchReducer(state, { type: 'SUCCESS', data: ['a', 'b'] });
expect(state.status).toBe('success');
if (state.status === 'success') {
expect(state.data).toEqual(['a', 'b']);
}
});

// 4. 测试边界条件(Guard)
it('重试次数超过上限不应生效', () => {
const state = fetchReducer(
{ status: 'error', error: 'timeout', retries: 3 },
{ type: 'RETRY' }
);
// 应保持 error 状态(假设有 retries < 3 的 Guard)
expect(state.status).toBe('error');
});
});

测试策略要点:

  • 覆盖所有合法转换:每条转换路径都要测
  • 覆盖非法转换:确保不应该发生的转换不会发生
  • 覆盖端到端流程:模拟完整的业务流程
  • 覆盖 Guard 条件:测试边界值和条件分支

相关链接