1. What 什么是状态机
有限状态机(英语:finite-state machine,缩写:FSM)又称有限状态自动机(英语:finite-state automation,缩写:FSA),简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学计算模型。
拿生活中的事情,来举一个例子🌰:
小明肚子饿了,想订外卖。小明打电话给商家,想吃脆皮炸鸡,商家接到电话,说这就马上做,承诺给小明做脆皮炸鸡。那么用两种情况:
- 炸鸡做好了,送到了小明手里,履行了承诺。
- 鸡腿原材料卖光了,无法给小明做脆皮炸鸡,于是拒绝。
转换成代码,可以这样描述:
- new Promise(‘商家答应做脆皮炸鸡’);
- Promise.resolve(‘做好了,送到小明手里’);
- Promise.reject(‘由于种种原因,无法完成’);
上面这个例子对应的 Promise
就是一个状态机,它是有限个、可以确定的状态。比如:
- PENDING - 等待中
- FULFILLED - 履行
- REJECTED - 拒绝
2. WHY 为什么要用状态机
可预测性!可预测性!可预测性!
在过去,我们的状态是隐式的 — 以布尔值的形式散落在各个角落。比如最常见的 isLoading
、 isFetching
等。
当我们想要改变状态时,状态机可以显式的提供更清晰的结构,并且提供何时改变、如何改变相关的说明,从而可以更容易的控制我们的程序。
好处如下,后面会详细解释:
- 语意化
- 可预测
- 降低复杂度
- 简洁、直观、易于理解
2.1 思维转变
通常来说,想开发一个应用,大多数开发者会使用事件动作的模式 (event-action) :当某个事件发生时,触发一个动作来响应。
而使用状态机,则是另一种模式:事件状态动作模式(event-state-action),当某个事件发生时,相应的过渡一个状态,根据新的状态触发一个动作。
这种思维转变非常重要,通过响应新的状态而不是某个事件,这使得我们的操作变得透明且易于追踪,并且不容易出错。
举一个生活中的例子🌰:
例如,某人单击一个按钮来提交付款,但是 UI 响应速度不够快,他们又单击了几次付款按钮。按照事件动作模式,如果我们没有正确地禁用按钮(或者如果用户足够聪明,可以通过控制台删除禁用的标志),我们最终可能会多次提交该付款请求。不好。
但是,遵循事件状态动作模式,当用户单击按钮提交付款时,我们的组件可能会过渡为加载状态(loading)。在该加载状态下,它不会响应任何新的提交操作。—— 从该加载状态,它只能转换为成功(success)或失败(error)。然后根据新的状态,响应下一步动作。
- 错误 -> 重新提交表单
- 成功 -> 成功的操作
由用户触发的操作 submit,进入 loading 状态,而 loading 状态的下一个状态只能是 success 或者 error。
这就是可预测性。我们的下一步操作是明确的,清晰的。
2.2 事件中的状态机
大多数现代框架(React,Vue,Angular 等)的组件驱动模型非常适合使用状态机。
通常,我更喜欢使用自己的状态机为每个有状态组件建模,并将该代码与我的组件代码组织在一起。这样,当其他人拿起我一直在研究的应用程序并开始阅读我的代码时,能很清晰的知道它是如何工作的。
现在,我们已经介绍了 What 和 Why,让我们深入研究如何实现(How)。在后面的例子中,我们将开发一个之前提到的付款提交表单。
3. How 如何使用状态机
3.1 建模组件行为
构建组件时,我要采取的第一步是使用状态图为该组件的行为建模。通常,我将从草绘纸上的状态图开始,以显示该组件应该能够存在的所有各种状态。
我们的付款表单应具有四个状态:
- 空闲(idle)
- 加载(loading)
- 成功(success)
- 失败(error)
空闲状态是表单的初始状态。当监听到 SUBMIT
事件时,我们将从空闲状态转换到加载状态。当我们进入加载状态时,我们将触发 POST
对后端服务的请求,然后根据该请求的结果转换为成功或错误状态。
我们可以使用如下状态图为上述行为建模:
如您在上图中所看到的,我们考虑了该模型的每个状态以及对应的转换。
如果我们碰巧处于空闲状态并监听到 PAYMENT_RECEIVED
事件,那么我们的状态机将不会转换为成功状态。它仅在监听到 SUBMIT
事件时才响应,这意味着我们确切地知道组件在任何给定场景下的行为。
3.2 从状态图到代码
现在,我们已经使用状态图对组件的行为进行了建模,我们可以开始将该图转换为代码了。
对于 JavaScript 中的状态机和状态图,我更喜欢使用 XState @GitHub。我们可以将状态图描述为 JavaScript 对象,并将其传递给 XState,它将提供一些方法和 helpers 帮我们执行代码。
因此,让我们来描述支付表单程序中的四个状态、以及它们之间如何过渡:
const stateMachine = Machine({
initial: 'idle',
states: {
idle: {
on: {
SUBMIT: 'loading',
},
},
loading: {
on: {
PAYMENT_SUCCESS: 'success',
PAYMENT_FAILED: 'error',
}
},
error: {
on: {
SUBMIT: 'loading',
},
},
success: {
type: 'final',
},
},
});
现在,我们使用代码描述好了我们的状态机,可以使用 XState 提供的可视化工具,检验一下状态描述是否正确。打开网址,把代码填上去:
该工具已经生成好可视化图表,是不是很方便!在图表中,可以点击蓝色的按钮,用来触发过渡状态。
3.3 Living Demo
具体实现看代码吧,这里不赘述。
4. Ref
- Hooks useMachine:https://github.com/carloslfu/use-machine
- XState:https://github.com/davidkpiano/xstate
- XState Docs:https://xstate.js.org/docs/
- XState Visualizer:https://xstate.js.org/viz