Open yinguangyao opened 3 years ago
在真实世界中,状态无处不在。红绿灯的颜色、人的喜怒哀乐、公司 OA 系统的审批流程等等都属于状态。 在 Web 开发中,状态也无处不在,选项卡的切换、loading 状态、开关按钮等等都有状态之间的切换。实际上我们前端开发中也是在和不同的状态打交道,知名前端框架 React 中就有 state 的概念。
先来看个红绿灯的例子,红绿灯一般会有三种状态,红灯表示禁止通行,绿灯表示准许通行,黄灯表示警示,这三种状态对应了不同的行为。 如果是用传统的方式来编写代码,那就是像下面这样,需要用一堆 if...else 或者 switch...case 来判断状态。
if...else
switch...case
class TrafficLight { constructor(state) { this.state = state; } switch() { if (this.state === 'green') { this.state = 'yellow'; console.log("警示"); } else if (this.state === 'yellow') { this.state = 'red'; console.log("禁止通行"); } else if (this.state === 'red') { this.state = 'green'; console.log("准许通行"); } } } const light = new TrafficLight("red"); setInterval(light.switch, 60 * 1000); // 假设每 60s 切换一次状态
这段代码主要就有下面这三个问题:
switch
状态模式是一种行为型模式,对象可以根据状态的改变来改变自己的行为。 一般来说,我们都是封装对象的行为,而非对象的状态。而在状态模式里面,就是把事物的每种状态都封装成独立的类,和状态有关的行为都在类的内部。在切换状态的时候,只需要在上下文中把请求委托给当前状态对象就行了。
以上面的代码为例,我们不需要在 switch 方法中进行判断、转换,只要把对应的状态转换关系封装在不同的状态类中就行了。
class RedLight { constructor(light) { this.light = light; } switch() { this.light.setState(this.light.greenLight); console.log("准许通行"); } } class YellowLight { constructor(light) { this.light = light; } switch() { this.light.setState(this.light.redLight); console.log("禁止通行"); } } class GreenLight { constructor(light) { this.light = light; } switch() { this.light.setState(this.light.yellowLight); console.log("警示"); } }
这里的 light 即上下文,在每次切换状态的时候它不会进行实质性的操作,而是委托给状态类来执行。
class Light { constructor() { this.greenLight = new GreenLight(this); this.redLight = new RedLight(this); this.yellowLight = new YellowLight(this); } setState(newState) { this.currentState = newState; } init() { this.currentState = this.redLight; } } const light = new Light(); light.init(); setInterval(light.currentState.switch, 60 * 1000)
从上述代码中就可以看出状态模式的优势,if...else 已经被消除了,我们不需要通过 if...else 来控制状态之间的切换。如果以后增加新的状态,只需要增加新的状态类就行了。 状态模式的缺点就是会在系统中创建大量的类,增加了系统的负担。同时逻辑分散在不同的状态类中,我们无法清晰地看出整个状态机的逻辑。
有限状态机(Finite-state machine)是编译原理中的一个概念,表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。
状态机这个词可能对于大家来说比较陌生,但在 Promise、generator 中都有状态机的影子。
有限状态机一般具有以下特点:
1)可以用状态来描述事物,在任何时刻,事物总是处于一种状态;
2)状态总数是有限的;
3)通过触发某种条件,事物可以从一种状态转变到另一种状态;
4)状态变化按照规则来,可以从 A 到 B,从 B 到 C,但不一定能从 A 到 C。
5)同一个条件,可以将事物从多个状态变成同一个状态,却不能从一种状态变成多种状态。
如果用公式来描述一下这个转换,那就是:
nextState = prevState + action
如果你有写过 React/Redux,会发现和他们的原理是如此相似,甚至 React 中也是用 state/setState 来命名状态/修改状态。
state/setState
在 web 开发中,我们也经常会遇到状态切换的场景,比如有个灯泡,点击打开按钮就会发亮,点击关闭按钮就会关闭;常用的菜单栏,鼠标移上去会展开,鼠标移开会隐藏等等。 如果结合状态模式来实现,那就是下面这样:
class Menu { constructor() { this.on = new On(this); this.pff = new Off(this); } setState(newState) { this.currentState = newState; } init() { this.currentState = this.on; const button = document.querySelector("#btn"); button.onclick = function() { this.currentState.click(); } } } class On { constructor(menu) { this.menu = menu; } click() { this.menu.setState(this.menu.off); } } class Off { constructor(menu) { this.menu = menu; } click() { this.menu.setState(this.menu.on); } }
上面这段代码虽然实现了功能,但还存在这么几个问题:
前面我们讲过表驱动,如果使用表来配置转换之间的转换关系呢?将状态的转换封装起来,我们只需要实现业务逻辑就行了。 现在已经有一个库帮你实现了这个功能。javascript-state-machine 是一个经典的有限状态机库,它的用法也比较接近表驱动。 我们可以定义初始状态、转换规则,以及其转换后的回调函数。
const fsm = new StateMachine({ init: 'off', transitions: [ { name: 'show', from: 'off', to: 'on' }, { name: 'hide', from: 'on', to: 'off' } ], methods: { onShow: function() { console.log('打开') }, onHide: function() { console.log('关闭') } }, error: function(eventName, from, to, args, errorCode, errorMessage) { return 'event ' + eventName + ': ' + errorMessage; }, });
javascript-state-machine 在使用的时候,常常需要生成一个实例,将初始状态、转换规则等属性当做配置传进去。
name
除了这些常规的 API 之外,javascript-state-machine 还提供了一系列的生命周期的钩子函数。
onBeforeShow
onAfterShow
onLeaveOff
onEnterOff
看到这里,你有没有觉得和 Vue/Mobx 很像?Vue 和 Mobx 都实现了例如 watch/reaction 这种功能,允许你监听某个属性的变化,自动去执行某些操作。 如果将 methods 改成 actions,也许你会发现这个理念和 redux 非常相似。 在我看来,不管是 react 还是 redux 都有状态机的思想在里面。在 react 中,local state 代表组件内部的状态,粒度比较细,常和一些 UI 上面的交互有关。 而在 redux 里面,global state 是一个更复杂的状态机,它常常用来管理一些全局的状态。
local state
global state
如果你有做过下拉分页列表的需求,那么一定会经常遇到这些问题。
整个流程的状态图如下:
可以看到,在代码中管理这么多状态本来就已经很头疼了,还要考虑处理各种转换的场景。
/* 真实的业务场景下远比这个要复杂 */ if (isLoading) { $("#loading").show(); // 请求接口... } else if (isError) { $("#loading").hide(); $("#error).show(); // 点击重新请求... } else if (isFinish) { $("#loading").hide(); $("#finish").show(); }
这么多状态之间的转换,如果用状态机来实现,是不是会清晰很多呢?
const fsm = new StateMachine({ init: 'noLoading', transitions: [ { name: 'error', from: 'loading', to: 'error' }, { name: 'finish', from: 'loading', to: 'finish' }, { name: 'tryAgain', from: 'error', to: 'loading' }, { name: 'loading', from: 'init', to: 'noLoading' } ], methods: { onEnterLoading() { $("#loading").show(); }, onLeaveLoading() { $("#loading").hide(); }, onEnterError() { $("#error").show(); }, onLeaveError() { $("#error").hide(); }, onEnterFinish() { $("#finish").show(); }, onTryAgain() { // 重新请求... } } } });
经过 javascript-state-machine 重构后的代码,虽然在这个例子中看起来代码量增加了,但这里将 DOM 操作和业务逻辑解耦开了,我们都不需要关心每次 DOM 的隐藏和展示,这在复杂业务下的可维护性会大大提高。
1. 前言
在真实世界中,状态无处不在。红绿灯的颜色、人的喜怒哀乐、公司 OA 系统的审批流程等等都属于状态。 在 Web 开发中,状态也无处不在,选项卡的切换、loading 状态、开关按钮等等都有状态之间的切换。实际上我们前端开发中也是在和不同的状态打交道,知名前端框架 React 中就有 state 的概念。
2. 状态模式
2.1 红绿灯的例子
先来看个红绿灯的例子,红绿灯一般会有三种状态,红灯表示禁止通行,绿灯表示准许通行,黄灯表示警示,这三种状态对应了不同的行为。 如果是用传统的方式来编写代码,那就是像下面这样,需要用一堆
if...else
或者switch...case
来判断状态。这段代码主要就有下面这三个问题:
switch
方法。2.2 状态模式重写红绿灯
状态模式是一种行为型模式,对象可以根据状态的改变来改变自己的行为。 一般来说,我们都是封装对象的行为,而非对象的状态。而在状态模式里面,就是把事物的每种状态都封装成独立的类,和状态有关的行为都在类的内部。在切换状态的时候,只需要在上下文中把请求委托给当前状态对象就行了。
以上面的代码为例,我们不需要在 switch 方法中进行判断、转换,只要把对应的状态转换关系封装在不同的状态类中就行了。
这里的 light 即上下文,在每次切换状态的时候它不会进行实质性的操作,而是委托给状态类来执行。
从上述代码中就可以看出状态模式的优势,
if...else
已经被消除了,我们不需要通过if...else
来控制状态之间的切换。如果以后增加新的状态,只需要增加新的状态类就行了。 状态模式的缺点就是会在系统中创建大量的类,增加了系统的负担。同时逻辑分散在不同的状态类中,我们无法清晰地看出整个状态机的逻辑。3. 状态机
3.1 什么是有限状态机?
有限状态机(Finite-state machine)是编译原理中的一个概念,表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。
状态机这个词可能对于大家来说比较陌生,但在 Promise、generator 中都有状态机的影子。
有限状态机一般具有以下特点:
1)可以用状态来描述事物,在任何时刻,事物总是处于一种状态;
2)状态总数是有限的;
3)通过触发某种条件,事物可以从一种状态转变到另一种状态;
4)状态变化按照规则来,可以从 A 到 B,从 B 到 C,但不一定能从 A 到 C。
5)同一个条件,可以将事物从多个状态变成同一个状态,却不能从一种状态变成多种状态。
如果用公式来描述一下这个转换,那就是:
如果你有写过 React/Redux,会发现和他们的原理是如此相似,甚至 React 中也是用
state/setState
来命名状态/修改状态。3.2 javascript-state-machine
在 web 开发中,我们也经常会遇到状态切换的场景,比如有个灯泡,点击打开按钮就会发亮,点击关闭按钮就会关闭;常用的菜单栏,鼠标移上去会展开,鼠标移开会隐藏等等。 如果结合状态模式来实现,那就是下面这样:
上面这段代码虽然实现了功能,但还存在这么几个问题:
前面我们讲过表驱动,如果使用表来配置转换之间的转换关系呢?将状态的转换封装起来,我们只需要实现业务逻辑就行了。 现在已经有一个库帮你实现了这个功能。javascript-state-machine 是一个经典的有限状态机库,它的用法也比较接近表驱动。 我们可以定义初始状态、转换规则,以及其转换后的回调函数。
javascript-state-machine 在使用的时候,常常需要生成一个实例,将初始状态、转换规则等属性当做配置传进去。
name
是触发这个转换规则时的条件方法。除了这些常规的 API 之外,javascript-state-machine 还提供了一系列的生命周期的钩子函数。
onBeforeShow
。onAfterShow
。onLeaveOff
。onEnterOff
。看到这里,你有没有觉得和 Vue/Mobx 很像?Vue 和 Mobx 都实现了例如 watch/reaction 这种功能,允许你监听某个属性的变化,自动去执行某些操作。 如果将 methods 改成 actions,也许你会发现这个理念和 redux 非常相似。 在我看来,不管是 react 还是 redux 都有状态机的思想在里面。在 react 中,
local state
代表组件内部的状态,粒度比较细,常和一些 UI 上面的交互有关。 而在 redux 里面,global state
是一个更复杂的状态机,它常常用来管理一些全局的状态。3.3 业务中的有限状态机
如果你有做过下拉分页列表的需求,那么一定会经常遇到这些问题。
整个流程的状态图如下:
可以看到,在代码中管理这么多状态本来就已经很头疼了,还要考虑处理各种转换的场景。
这么多状态之间的转换,如果用状态机来实现,是不是会清晰很多呢?
经过 javascript-state-machine 重构后的代码,虽然在这个例子中看起来代码量增加了,但这里将 DOM 操作和业务逻辑解耦开了,我们都不需要关心每次 DOM 的隐藏和展示,这在复杂业务下的可维护性会大大提高。
推荐阅读