本文最后更新于:2020年7月15日 早上
xstate.js 官网, 中文(繁体)教程参考
场景
- 为什么要引入状态机?
- 吾辈希望使用有限状态机管理程序中的状态及状态的流转,以避免使用各种
flag
+ if/else
控制程序的运行。
- 为什么吾辈会突然觉得
flag
+ if/else
这种方式不好呢?
- 原因在于吾辈最近在看设计模式相关的书籍:JavaScript 设计模式与开发实践,其中涉及到了[状态模式],里面就提到了[有限状态机]与[状态图]的概念,在经过 Google 一下了解之后,吾辈确实感觉到可以使用它来简化程序的状态流转控制。
例如有一个开关,控制灯泡怎么变化,在指定状态下点击会触发不同的行为,然后改变状态。
基本示例
想象以下场景,有一个开关控制着一个灯泡,灯泡有三种状态:关闭、打开弱光和打开强光,轮流变化。
使用原生代码实现
首先,我们尝试简单的使用 if/else
判断进行实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class Light { private state: "off" | "weak" | "strong" = "off"; click() { switch (this.state) { case "off": console.log("打开弱光"); this.state = "weak"; break; case "weak": console.log("打开强光"); this.state = "strong"; break; case "strong": console.log("关灯"); this.state = "off"; break; } } }
const light = new Light(); light.click(); light.click(); light.click();
|
然而,这种代码充斥着判断,同时代码本身也都耦合在了一起。目前只有一个 flag
的时候还没太大问题,如果有更多的 flag(例如典型的用户角色与状态同时控制指定操作的行为),代码将非常混乱。
这也是吾辈之前为什么很想要一种支持多个 key 的 Map 的重要原因之一(其实有点类似数据库中索引的概念了)。
抽离方法,使用 Map 消除判断
一般来说,我们都会使用下面的方式去简化代码。
- 将不同的逻辑抽离为单独的方法
- 使用 Map 消除条件判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| class Light { private state: "off" | "weak" | "strong" = "off"; private map = { off: this.offClick.bind(this), weak: this.weakClick.bind(this), strong: this.strongClick.bind(this), }; click() { this.map[this.state](); }
private offClick() { console.log("打开弱光"); this.state = "weak"; } private weakClick() { console.log("打开强光"); this.state = "strong"; } private strongClick() { console.log("关灯"); this.state = "off"; } }
const light = new Light(); light.click(); light.click(); light.click();
|
事实上,第一种方式是卓有成效且任何人都能够无师自通的(分离了实现和控制),但第二种,虽然这里可以简化逻辑的控制,但稍微复杂或是由多个 flag
控制的状态流转则无法使用这种方式抽离出来。
使用简单的状态模式
下面是用简单的状态模式来简化代码的控制逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class Light { toggle: (light: Light) => void = FSM.off; click() { this.toggle(this); } }
const FSM = { off(light: Light) { console.log("打开弱光"); light.toggle = FSM.weak; }, weak(light: Light) { console.log("打开强光"); light.toggle = FSM.strong; }, strong(light: Light) { console.log("关灯"); light.toggle = FSM.off; }, };
const light = new Light(); light.click(); light.click(); light.click();
|
仔细观察变化
- 主流程只是做了转发,将操作转发给当前状态的子流程执行
- 状态对应的操作都在子流程中修改
其实本质上状态模式是将控制流程分散到了各个子流程中,不再集中在一个地方控制。
结合 react 与状态模式
然后,有趣的地方来了:如何结合状态模式与现有框架?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| import React from "react"; import ReactDOM from "react-dom"; import { useState } from "react";
class Light { toggle: (light: Light) => void = FSM.off; click() { this.toggle(this); } }
const FSM = { off(light: Light) { console.log("打开弱光"); light.toggle = FSM.weak; }, weak(light: Light) { console.log("打开强光"); light.toggle = FSM.strong; }, strong(light: Light) { console.log("关灯"); light.toggle = FSM.off; }, };
function App() { const [light] = useState(new Light()); return ( <div> <h2>app</h2> <button onClick={() => light.click()}>灯的开关</button> </div> ); }
ReactDOM.render(<App />, document.querySelector("#app"));
|
然而,当我们想要即时显示当前状态时,却发现了问题,是的,状态模式中将状态放在 Light
类中,而它并不在 react 的控制范围之内!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| // 其他代码
class Light { state: "off" | "weak" | "strong" = "off"; // 其他代码 }
const FSM = { off(light: Light) { console.log("打开弱光"); light.state = "weak"; light.toggle = FSM.weak; }, // 其他状态的代码 };
function App() { const [light] = useState(new Light()); return ( <div> <h2>app</h2> <button onClick={() => light.click()}>灯的开关</button> <p>{light.state}</p> </div> ); }
// 其他代码
|
即便这样做,仍然不会发生变化,因为 Light 是个对象,而我们并未使用 setLight
修改它,所以自然不会发生变化
尝试 react + useReducer
当然,我们可以使用 useReducer
试试,毕竟它是专门应对复杂逻辑处理的 hooks。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| type LightState = 'off' | 'weak' | 'strong' const FSM: Record<LightState, () => LightState> = { off(): LightState { console.log('打开弱光') return 'weak' }, weak(): LightState { console.log('打开强光') return 'strong' }, strong(): LightState { console.log('关灯') return 'off' }, }
const App: React.FC = () => { const [num, setNum] = useState(0) const [lightState, lightSend] = useReducer<(state: LightState) => LightState>( (state) => { return FSM[state]() }, 'off', )
return ( <div className="App"> <header> <button onClick={() => lightSend()}>切换</button> 每次从 off => weak 就改变状态 </header> <p>{lightState}</p> <p>当前 useState 的值: {num}</p> </div> ) }
|
然而,可以看到,FSM 逻辑代码在 react 组件外部时,想要修改 react 组件内部的状态仍然非常困难,只能维护状态机自身的状态,而这显然是没多大用处的。解决方案很简单,使用回调的形式将具体的实现函数放在 react 组件内部,而这,正是 xstate 集成 react 实现的功能之一。
使用 xstate
react + 状态机 xstate
下面是一个使用状态机控制点击开关控制灯泡的示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| enum LightStateEnum { Off = 'off', Weak = 'weak', Strong = 'strong', }
enum LightEventEnum { Click = 'click', } interface LightEvent extends EventObject { type: LightEventEnum.Click num: number }
enum LightActionEnum { EntryWeak = 'entryWeak', }
const lightStateMachine = Machine<{}, LightEvent>({ initial: LightStateEnum.Off, states: { [LightStateEnum.Off]: { on: { [LightEventEnum.Click]: { target: LightStateEnum.Weak, actions: LightActionEnum.EntryWeak, }, }, }, [LightStateEnum.Weak]: { on: { [LightEventEnum.Click]: LightStateEnum.Strong, }, }, [LightStateEnum.Strong]: { on: { [LightEventEnum.Click]: LightStateEnum.Off, }, }, }, })
const App: React.FC = () => { const [num, setNum] = useState(0) const [lightState, lightSend] = useMachine(lightStateMachine, { actions: { [LightActionEnum.EntryWeak](_context, event) { console.log('entryOff: ', event.num) setNum(num + event.num) }, }, }) return ( <div className="App"> <header> <button onClick={() => lightSend({ type: LightEventEnum.Click, num: 1, }) } > 切换 </button> 每次从 off => weak 就改变状态 </header> <p>{lightState.value}</p> <p>当前 useState 的值: {num}</p> </div> ) }
|
看的出来,上面多了很多模板代码,但状态机的意图我们却能以声明式的形式构造出来,具体 actions
的实现细节则被分离在 hooks 中。
xstate 概述
基本概念
常用术语, 术语参考
- 状态: 任何时候总是有且只有一种状态
- 事件: 对外暴露事件,通过事件(声明式)触发状态的变化
- 动作: 触发事件时对应的具体行为,可以以编程的形式影响状态的变化
问题
- 主要问题还是太大了,xstate 想要把所有的状态都用这种方式管理起来,而非仅限于适合的情况。。。(大而全 )
在线示例
- [x] 怎么让 TypeScript 提示正确的类型?
- [x] 怎么在 react 里使用它修改状态
- 使用
useMachine
在 react hooks 组件里添加 actions