状态机(State Machine)是编程中经常遇到的一个概念。登录流程、表单提交、订单系统、动画播放,都可以用它来描述。
它的完整名称是 "有限状态机"(Finite State Machine,缩写 FSM),源自自动机理论,是计算机科学里的一个经典模型,编译器、正则表达式引擎、网络协议、游戏开发里都能看到它的身影。
这篇文章想把这几个概念讲清楚:状态机是什么、为什么要用它、代码里怎么写。
一、先从"状态"说起
状态描述的是"某个东西在当前时刻所处的情况",不是动作。
比如一个人:
- 在睡觉
- 在上班
- 在吃饭
- 在回家路上
这几个词,都是这个人的状态。
再比如一个订单:
- 待支付
- 已支付
- 已发货
- 已签收
- 已取消
这些也是状态。
容易混淆的地方是,把「状态」和「动作」弄混。点击按钮、发起请求、提交表单,这些是动作,不是状态。状态是动作发生之后系统停留的位置------比如"请求进行中""提交失败"。
二、状态机是什么
状态机由三部分组成:
- 有哪些状态
- 什么事件触发变化
- 状态之间怎么切换
可以写成一个公式:
状态机 = 状态 + 触发条件 + 转换规则
红绿灯就是一个状态机。它有三个状态:红灯、绿灯、黄灯。转换规则是:
- 红灯结束,切到绿灯
- 绿灯结束,切到黄灯
- 黄灯结束,切到红灯
这就是一个完整的状态机。
三、为什么叫"机"
这里的"机"指的是"机制",不是机器。
状态机强调状态变化是有规则的:
- 状态是固定的、有限的(这也是"有限状态机"里"有限"二字的来源)
- 每次变化都需要触发条件
- 变化路径是确定的,不能随意跳转
所以状态机不是在说"状态发生了变化",而是在说:系统会按照一套明确的规则,在有限的几个状态之间切换。
四、生活里的状态机
1. 自动售货机
它可能有这些状态:
- 待机
- 已投币
- 已选择商品
- 出货中
- 退款中
切换过程大概是这样:
css
flowchart TD
A[待机] --> B[已投币]
B --> C[已选择商品]
C --> D[出货中]
B --> E[退款中]
E --> A
D --> A
用户投币,状态从"待机"切到"已投币";选了商品,继续切到"已选择商品";钱不够,则根本不能切到"出货中"。
这里有个重要特点:不是任何时候都可以做任何事。在"待机"状态下不能直接"出货",必须先投币、再选商品,才可能走到下一步------当前处于什么状态,决定了下一步能做什么。
2. 电梯
电梯也是状态机,可能有这些状态:静止、上行、下行、开门、关门。
如果电梯正在上行,这时按"开门"通常不会立刻开,因为"上行"状态下不允许直接进入"开门",必须先停下,再开门。
五、代码里的状态机
用红绿灯举例,写成代码大概是这样:
kotlin
const trafficLight = {
state: 'red',
transitions: {
red: 'green',
green: 'yellow',
yellow: 'red',
},
next() {
this.state = this.transitions[this.state];
return this.state;
},
};
trafficLight.next(); // 'green'
trafficLight.next(); // 'yellow'
trafficLight.next(); // 'red'
这段代码是状态机三要素的直接体现:state 是当前状态,transitions 是转换规则,next() 触发一次切换。
真实项目里,状态往往还要携带上下文数据(比如订单号、错误信息),转换也可能需要满足条件(比如余额是否够)、执行副作用(比如发一次请求)。这种情况下通常会引入专门的状态机库,比如前端常用的 XState,或者用 Redux 这类状态管理工具搭配 reducer 实现类似的约束。思路是一致的:明确列出所有状态,明确列出所有合法的转换,不允许其他情况发生。
六、程序里的状态天然存在
程序的很多功能不是"一下子完成"的,而是要经历好几个阶段。
比如登录:未登录 → 登录中 → 已拿到 token → 已加载权限 → 已进入系统。
比如表单提交:未编辑 → 编辑中 → 提交中 → 提交成功 / 提交失败。
比如订单系统:待支付 → 已支付 → 待发货 → 已发货 → 已完成。
只要一个系统存在"阶段",通常就适合用状态机来理解。
七、以登录流程为例
初学者理解登录,容易简化成"调一下登录接口,成功就结束了"。
实际过程更接近这样:
- 用户还没登录
- 用户输入账号密码
- 前端发起登录请求
- 后端返回 token
- 前端保存 token
- 路由守卫识别到用户已登录
- 系统拉取权限和菜单
- 页面进入后台首页
画出来大概是这样:
css
flowchart TD
A[未登录] --> B[登录中]
B --> C[拿到 token]
C --> D[保存 token]
D --> E[加载权限菜单]
E --> F[进入后台]
如果某一步失败------token 没存成功、菜单没加载成功、路由守卫没识别出来------系统就不会进入最后的"进入后台"状态。
这时候看到的现象可能是:登录接口明明成功了,但页面没进去;能进首页,但菜单是空的;一刷新又回登录页。这类问题通常不是单个接口的问题,而是状态没有走通。
八、状态机和流程图的区别
常见的疑问是:这不就是流程图吗?两者确实相似,但关注点不同。
流程图关注的是"第一步做什么、第二步做什么",描述的是过程。
状态机关注的是"当前处于什么状态、这个状态下允许发生什么、满足什么条件才能切到下一个状态",描述的是系统此刻的身份。
以"提交订单"为例,流程图会说:点击提交 → 校验参数 → 调接口 → 返回结果。状态机会说:当前是"待提交",点击后进入"提交中",成功则变成"已提交",失败则变成"提交失败"。
两者不冲突,但 排查"系统表现为什么不对"时,状态机的视角通常更有效。
九、容易忽略的一点:非法状态切换
不是所有状态都能直接互相切换。
比如订单系统里,"待支付"可以切到"已支付","已支付"可以切到"已发货",但"待支付"通常不能直接切到"已签收"。
代码里如果出现了这种跳转,往往就是逻辑出了问题------可能是漏判断了某个中间状态,也可能是异常分支没处理。这也是状态机思维在排查 bug 时比较实用的地方:先确认当前状态,再确认这次切换是否合法,问题范围会缩小很多。
十、什么时候适合用状态机
下面这些场景比较适合用状态机来想问题:
- 登录和权限
- 订单和支付
- 表单提交
- 审核流
- 文件上传
- 多步骤向导
- 弹窗、抽屉、动画切换
不过不是所有代码都需要显式建模成状态机。如果一个功能只有一两个状态、没有复杂的分支和中间态,硬套状态机反而会增加不必要的抽象成本。是否值得用,取决于状态的数量和转换规则的复杂度------状态越多、非法转换的代价越高(比如订单、支付这类涉及资金的场景),越值得花时间把它显式定义清楚。
十一、总结
状态机不是什么高深理论,它是一种描述系统的方法:先定义系统可能存在的所有状态,再定义状态之间的合法转换规则。
写代码时,精力容易都花在"过程"上------先做什么、再做什么。但很多复杂问题的根源不在过程,而在状态没想清楚:系统卡在了一个不该停的状态,或者发生了一次不该发生的转换。
遇到这类问题时,多问一句"系统现在处于什么状态",往往比继续追问"程序做了什么"更容易找到答案。
延伸阅读: