状态转换
转换形式
状态转换发生一般发生在执行某个操作或者某个事件行为产生时,我们将导致状态发生变化的操作或事件称副作用。而副作用可能是来自外部事件,也可能是来自主动执行某些操作产生的。
基于副作用的产生,状态转换存在以下两种形式:
主动转换
- 主动转换: 执行动作
执行某个会产生副作用的动作而导致状态发生变化,例如调用本地的connect
方法来连接服务器,由于连接是一个异步过程,因此状态会依次发生变化,从Disconnected->Connecting->Connected
。 主动转换对状态的转换是可预知的。比如我们知道调用connect
方法时,要么进入已连接,要么进入错误或断开状态,对最终的状态是可以预知的。
被动转换
- 被动转换 :响应外部事件
基于某个已经产生副作用的事件而导致状态发生变化。例如服务器主动断开客户端连接时,会在客户端会触发close/end
事件,从而导致客户端的状态从Connected->Disconnected
。 被动转换则是不可预知的。比如我们不知道服务器什么时候会将客户端断开。
两者区别
在现实业务中,主动转换
和被动转换
往往同时存在,所以必须了解两者的区别,在编程时进行兼容处理,否则很容易导致状态混乱。两者主要区别在于:
主动转换
是基于尚未或准备发生的业务进行的主动转换,因此可以在状态转换钩子中进行拦截中止;而被动转换是基于已经发生的事实,一般不允许进行拦截中止;比如服务器已经中断了连接,则客户端则一定应该处于Disconnected
状态,所以在onDisconnectedEnter
进行拦截阻止进入Disconnected
就没有意义。主动转换
和被动转换
往往会交织在一起,比如connect
方法,你可以在发起连接操作前onConnectingEnter
进行拦截干预,但是一旦连接成功,触发了connect
事件,则一定会进入Connected
状态,因此不应该在onConnectingLeave
中进行拦截阻止离开Connecting
状态。我们可以看出,执行connect
操作副作用, 导致了状态机同时产生主动转换和被动转换。- 被动转换场景下,我们需要在事件已经发生时,调用状态机的
transition
方法来转换至某个确定状态,相当于直接修改状态机的状态,并且特别需要注意的是:不要在转换钩子中进行拦截阻止,否则状态将会不正确,因为状态转换的事实已经发生。如上例中当socket
已经触发close/end
事件了,说明事实上状态机已经处于disconnected
状态了,此时在转换钩子中拦截就没有意义了。 - 主动转换场景下,则存在操作失败出错或者被拦截取消等,因此不应该直接调用
transition
方法,而是引入动作(Action)
来转换状态(详见下节关于Action
的说明)。
可以看出,主动转换
与被动转换
的最核心的区别在于副作用是事实存在的还是尚未发生的。
转换过程
状态转换分三个阶段:
第1步:离开当前状态
触发<当前状态>/leave
事件,执行<当前状态>/leave
钩子函数。如果leave
钩子函数执行成功则代表成功离开当前状态。如果leave
钩子函数执行出错,则代表无法离开当前状态,将根据错误类型来决定如何处理:
- 如果抛出的是普通的
ERROR
,一般代表了在钩子函数中没有产生副作用或者产生的副作用可消除,因此当前状态将保持不变。 - 如果抛出的是
SideEffectTransitionError
,则代表产生了产生不可消除的副作用,状态机将强制转换到ERROR
状态。
第2步:进入目标状态前
在转换到目标状态前,会触发<目标状态>/enter
事件,执行<目标状态>/enter
钩子函数。如果enter
钩子函数执行出错,则代表无法进入目标状态。但是由于在执行<当前状态>/leave
钩子函数时可能产生副作用,因此将触发<当前状态>/resume
事件,开发者应该在<当前状态>/resume
钩子函数中消除执行<当前状态>/leave
钩子函数时可能产生副作用。
- 如果在
<当前状态>/resume
钩子函数中成功消除了产生的副作用(没有触发错误),则将恢复到原始状态。 - 如果
<当前状态>/resume
钩子函数中触发了错误,则说明无法消除副作用,因此状态机将转换至ERROR
状态。
第3步:已转换至目标状态
当<当前状态>/leave
和<目标状态>/enter
均成功后,状态机将转换至目标状态,并且触发<目标状态>/done
事件。注意可以订阅<目标状态>/done
事件在转换完成后做一些事情,但是无法在done钩子
中通过触发错误来阻止转换过程。状态机将忽略done钩子函数
中的所有错误。
转换方法
状态机提供transition
方法来从当前状态转换至其他状态,该方法可以在被动场景下使用,即当接收到指定事件时调用本方法来转换状态。当执行transition
方法时,会根据state.next
和状态转换拦截钩子
约束下来确认是否可以成功转换到目标状态。也就是说转换能否成功,取决于目标状态是否在当前状态的next
参数中,以及拦截钩子是否进行拦截。例如:
states.A.next=["B","C"]
代表可以从A状态转换到B和C状态,那么调用transition("D")
就会出错。- 如果A状态转换到B状态时,A/leave和B/enter两个钩子函数返回false或触发错误,则转换也会被阻止。更详细见下文介绍。
transition
方法签名如下:
async transition(next:FlexStateArgs,params={})
- next:要转换的目标状态名称或状态值
- params:用来传递给转换钩子函数的参数
await fsm.transition("<目标状态名称>")
// fsm.current.name==="Initial" , states.Initial.next=["Connecting","Disconnected","Error"]
await fs.transition("Connecting") // 转换成功,
await fs.transition("Connected") // 转换失败
await fs.transition("Disconnecting") // 转换失败
转换成功时
- 当前状态转换到目标状态,即
fsm.current.name === "<目标状态名称>"
- 转换过程会触发事件
<当前状态>/leave
、<目标状态>/enter
、<目标状态>Done
- 依次调用钩子函数
on<当前状态>Leave
、on<目标状态>Enter
、on<目标状态>Done
、on<目标状态>
- 当前状态转换到目标状态,即
转换失败时
- 触发
TransitionError
- 在转换阶段中触发执行不同的钩子函数,详见上文转换过程说明。
- 触发
最佳实践
当主动转换状态时应该采用执行动作的办法,而不应该调用fs.transition
;
仅在被动状态变化时才调用fs.transition
方法。
转换限制
- 同一时刻只能存在一个转换过程
由于有限状态机同一时刻只能处于某个状态或者处于状态转换过程中。因此,不能在从A状态转换至B状态时,又同时要转换C状态。通过fsm.transitioning
属性可以查询当前是否正在转换中,如果fsm.transitioning==true
时,再调用fsm.transition({...})
则会触发TransitioningError
- 状态机启动后才能进行转换
仅当调用了start
启动状态机后才能进行状态转换和执行动作。默认情况下,状态机的autoStart=true
,也就是说实例化状态机后会自动启动。
- 状态处于FINAL状态时,不允许再进行状态转换
当状态机处于任一个FINAL
状态时,将不能再转换至其他状态,也不能执行动作。
转换约束
每个状态的next
参数可以用来约束该状态只能转换到其他什么状态。next
参数取值支持:
*
:代表可以转换至任意状态[<状态名称>,....,<状态名称>]
:代表只能转换到其中任一个状态<状态名称>,....,<状态名称>
:代表只能转换到其中任一个状态函数
:返回状态名称或者状态名称列表,代表只能转换到该函数返回的其中任一个状态