zhangdizhangdi

状态模式 📖

定义

状态模式(State Pattern) 是我们在处理“复杂业务流转”和“组件生命周期”时的一把利器。从本质上讲,状态模式就是面向对象版本的“有限状态机(FSM)”

允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。

当你按下键盘的“空格键”时,播放器会发生什么?如果当前是 “播放状态” ,按下空格会暂停;如果当前是 “暂停状态” ,按下空格会继续播放。同一个动作(按空格),在不同的状态下,产生了完全不同的行为。

痛点:当一个对象有多种状态,且不同状态下的行为极其复杂时,代码中会充斥着形如 if (state === 'PLAYING') { ... } else if (state === 'PAUSED') { ... } 的巨型判断逻辑。且状态之间的切换极易产生 Bug。
解决:将每一种状态封装成一个独立的类/对象。把各种情况下的行为分散到这些状态对象中。上下文对象(Context)只需要把动作委托给当前的状态对象去执行。

对比:状态模式 vs 策略模式

由于两者的类图几乎一模一样(都是把逻辑委托给另一个对象),这是面试中的高频考点。核心区别在于 “谁来掌控变化”“变化的驱动力”

维度 策略模式 (Strategy) 状态模式 (State)
独立性 各个策略之间是平行且毫无关系的。 各个状态之间是有流转关系的(A状态能切到B状态)。
谁来切换 客户端(外界)主动切换。比如你主动在地图里选择“公交”还是“打车”。 上下文内部或状态对象自身来切换。比如播放完自动切到停止状态,外界并不干涉。
业务认知 客户端需要了解有哪些策略可用。 客户端通常只需要告诉环境“去执行动作”,不需要知道当前内部是什么状态。

实现

1. 传统 OOP 经典实现

上下文(播放器)把动作委托给当前状态。状态执行完动作后,主动去修改上下文的状态

javascript
// 1. 状态基类 (规定有哪些动作)
class State {
  constructor(player) {
    this.player = player; // 互相持有引用,为了改变状态
  }
  pressPlay() { throw new Error('必须实现 pressPlay'); }
}

// 2. 具体状态类:播放中状态
class PlayingState extends State {
  pressPlay() {
    console.log('当前正在播放 -> 执行暂停');
    this.player.setState(this.player.pausedState); // 状态内部驱动状态流转!
  }
}

// 2. 具体状态类:暂停状态
class PausedState extends State {
  pressPlay() {
    console.log('当前已暂停 -> 恢复播放');
    this.player.setState(this.player.playingState); 
  }
}

// 3. 上下文:音乐播放器
class AudioPlayer {
  constructor() {
    // 初始化所有可用状态
    this.playingState = new PlayingState(this);
    this.pausedState = new PausedState(this);
    // 设置初始状态
    this.currentState = this.pausedState; 
  }

  setState(state) {
    this.currentState = state;
  }

  // 暴露给外界的方法:把请求委托给当前状态!
  pressPlayButton() {
    this.currentState.pressPlay();
  }
}

// ================= 使用 =================
const player = new AudioPlayer();
player.pressPlayButton(); // 输出: 当前已暂停 -> 恢复播放
player.pressPlayButton(); // 输出: 当前正在播放 -> 执行暂停
player.pressPlayButton(); // 输出: 当前已暂停 -> 恢复播放
// 客户端全程只需按按钮,根本没有 if-else!

2. 现代 JS 的状态机实现 (FSM)

在前端(JS环境),由于可以非常方便地使用字面量对象,我们很少写上面那么多类,而是通常将其实现为一个 有限状态机 (Finite State Machine, FSM) 的配置表。

javascript
// 更符合前端口味的“状态机配置表”
const fsmPlayer = {
  state: 'paused', // 初始状态

  // 状态转移字典表
  transitions: {
    paused: {
      pressPlay: function() {
        console.log('当前已暂停 -> 恢复播放');
        this.state = 'playing'; // 切换状态
      }
    },
    playing: {
      pressPlay: function() {
        console.log('当前正在播放 -> 执行暂停');
        this.state = 'paused';
      }
    }
  },

  // 对外触发接口
  dispatch(actionName) {
    const action = this.transitions[this.state][actionName];
    if (action) {
      action.call(this); // 执行并绑定 this
    } else {
      console.log(`警告:在 ${this.state} 状态下不支持动作 ${actionName}`);
    }
  }
};

fsmPlayer.dispatch('pressPlay'); // 暂停 -> 播放
fsmPlayer.dispatch('pressPlay'); // 播放 -> 暂停

前端应用

1. 前端最重要的基石 —— Promise

Promise 就是一个最经典、最严格的状态模式应用!

Promise 内部有三种状态:Pending(进行中)、Fulfilled(已成功)、Rejected(已失败)。

  • 它的状态机有着极其严格的单向流转规则:只能从 Pending → Fulfilled 或 Pending → Rejected。
  • 一旦到达终态,调用 resolve()reject() 将不会产生任何行为。这就是典型的“不同状态下行为不同”。

2. XState 与复杂 UI 状态管理

在复杂的 React / Vue 开发中(比如电商结账流程、多步骤表单、拖拽交互),单纯依靠定义一堆 isLoading, isError, isSuccess 极其容易引发状态组合爆炸(比如同时出现 isLoading=trueisError=true 的幽灵 Bug)。

前端目前非常流行使用 XState 这样的专业状态机库。开发者通过画“状态图”,严格定义组件在“Idle”、“Loading”、“Success”、“Failure”等互斥状态之间的流转路径,彻底消灭了不合法的 UI 状态。

3. 游戏开发中的角色控制器

在用 Canvas 或 WebGL 做 HTML5 游戏时,角色往往有“站立(Idle)”、“走(Walk)”、“跳跃(Jump)”、“攻击(Attack)”等状态。

如果不用状态模式:

javascript
// 满屏的 flag 噩梦
if (isJumping && !isAttacking) {
  // 如果在跳跃,就不能再次跳跃
} else if (isWalking && key === 'space') {
  // 走的时候可以跳
}

引入状态模式后,将每个行为封装成 JumpState 等。在 JumpState 内,无论你怎么按“跳跃键”,状态机都会将其忽略,逻辑异常清晰。

4. 复杂的组件交互(例如下拉刷新组件)

拉到底部的“上拉加载更多”组件,其实是一个非常标准的状态机。它内部可能包含四个状态:

  1. normal(正常):监听滚动事件。
  2. pulling(拉拽中):文案显示“松开加载”。
  3. loading(加载中):展示 Loading 图标,在此状态下忽略一切外部拉拽和点击动作
  4. no-more(到底了):展示“没有更多了”,彻底禁用加载行为。

总结

当你发现某个组件内部出现了两三个甚至更多的 Boolean 标志位(如 isA, isB),并且大量函数内部都需要通过 if (isA && !isB) 来决定是否执行逻辑时,这通常是系统在向你求救。
此时,请果断祭出状态模式(或有限状态机 FSM),用确定的状态枚举替换掉散乱的布尔值,将行为封装到各个状态配置中,你会发现原本错综复杂的代码瞬间变得井然有序。