静下心来的个人专栏
上一篇

Javascript状态机模型

广告
选中文字可对指定文章内容进行评论啦,→和←可快速切换按钮,绿色背景文字可以点击查看评论额。
大纲

状态机模型

状态机是管理有限数量的状态和从一种状态转换到另一种状态的事件的模型。它们是允许我们明确定义应用程序路径而不是防范一百万条可能路径的抽象。

应用举例

例如,交通信号灯的状态机将保持三种状态:红色、黄色和绿色。绿色变为黄色,黄色变为红色,红色变为绿色。让状态机定义我们的逻辑,就不可能有从红色​​到黄色或黄色到绿色的非法转换。

为了了解状态机如何极大地降低我们的 UI、业务逻辑和代码的复杂性,我们将采用以下示例:

我们有一个可以点亮、关闭或坏掉的灯泡,以及三个可以打开或关闭或破坏灯泡的按钮,如下面的 HTML 代码所示:

<p>The light bulb is <span id="lightbulb">lit</span></p>

<button id='turn-on'>turn on</button>
<button id='turn-off'>turn off</button>
<button id='break'>break</button>

我们将引用我们的按钮元素并添加点击事件监听器来实现我们的业务逻辑。

const lightbulb = document.getElementById("lightbulb")

const turnBulbOn = document.getElementById("turn-on")
const turnBulbOff = document.getElementById("turn-off")
const breakBulb = document.getElementById("break")

turnBulbOn.addEventListener("click", () => {
  lightbulb.innerText = "lit"
})

turnBulbOff.addEventListener("click", () => {
  lightbulb.innerText = "unlit"
})

breakBulb.addEventListener("click", () => {
  lightbulb.innerText = "broken"
})

如果灯泡坏了,再次打开它应该是不可能的状态转换。但是上面的实现允许我们通过简单地点击按钮来做到这一点。所以我们需要提防从broken状态到lit状态的转换。为此,我们经常在每个操作之前使用布尔标志或详细检查,如下所示:

let isBroken = false

turnBulbOn.addEventListener("click", () => {
  if (!isBroken) {
    lightbulb.innerText = "lit"
  }
})

turnBulbOff.addEventListener("click", () => {
  if (!isBroken) {
    lightbulb.innerText = "unlit"
  }
})

breakBulb.addEventListener("click", () => {
  lightbulb.innerText = "broken"
  isBroken = true
})

但这些检查通常是我们应用程序中未定义行为的原因:提交失败但显示成功消息的表单,或者不应该触发的查询。编写布尔标志非常容易出错,并且不容易阅读和推理。更不用说我们的逻辑现在分布在多个范围和函数中。重构这种行为,意味着重构多个函数或文件,使其更容易出错。

这是状态机非常适合的地方。通过预先定义应用程序的行为,您的 UI 仅反映了应用程序的逻辑。因此,让我们重构我们的初始实现以使用状态机。

状态和事件

我们将我们的狀態機的状态定义为一个简单的对象,它描述了可能的状态及其转换。许多状态机库求助于更复杂的对象来支持更多功能。

const machine = {
  initial: "lit",
  states: {
    lit: {
      on: {
        OFF: "unlit",
        BREAK: "broken",
      },
    },
    unlit: {
      on: {
        ON: "lit",
        BREAK: "broken",
      },
    },
    broken: {},
  },
}

这是我们状态机的可视化:

我们的状态机对象包含一个初始属性,用于在我们第一次打开我们的应用程序时设置初始状态,以及一个包含应用程序可能处于的每个可能状态的状态对象。这些状态反过来有一个可选的对象,说明它们反应的事件。点亮状态对 OFF 和 BREAK 事件作出反应。这意味着当灯泡点亮时,我们可以将其关闭或断开。我们不能在它打开时打开它(尽管如果我们选择这样做,我们可以以这种方式模拟我们的逻辑。)

最终状态:

破坏状态不会对任何使其成为最终状态的事件做出反应——这种状态不会转换到其他状态,从而结束整个状态机的流程。

状态转换:

转换是一个纯函数,它根据当前状态和事件返回一个新状态。

const transition = (state, event) => {
  const nextState = machine.states[state]?.on?.[event]
  return nextState || state
}

转换函数遍历我们机器的状态及其事件:

transition('lit', 'OFF')      // returns 'unlit'
transition('lit', 'BREAK')    // returns 'broken'
transition('unlit', 'OFF')    // returns 'unlit' (unchanged)
transition('broken', 'BREAK') // return  'broken' (unchanged)
transition('broken', 'OFF')   // returns 'broken' (unchanged)

如果我们不在给定状态下处理特定事件,我们的状态将保持不变,返回旧状态。这是状态机背后的一个关键思想。

追踪和发送事件

为了跟踪和发送事件,我们将定义一个默认为我们状态机初始状态的状态变量和一个接收事件并根据我们狀態機的实现更新我们的状态的发送函数。

let state = machine.initial
const send = (event) => {
 state = transition(state, event)
}

所以,现在我们不再通过布尔值和 if 语句来跟上我们的状态,而是简单地调度我们的事件并调用我们的 UI 来更新:

turnBulbOn.addEventListener("click", () => {
 send("ON")
 lightbulb.innerText = state
})
turnBulbOff.addEventListener("click", () => {
 send("OFF")
 lightbulb.innerText = state
})
breakBulb.addEventListener("click", () => {
 send("BREAK")
 lightbulb.innerText = state
})

我们的三个按钮将按预期工作,每当我们打破灯泡时,用户将无法打开或关闭它。

总结:

当一个对象的状态有限,并且在执行的过程中状态转化需要很多逻辑判断的时候,这时候抽象出一个状态机往往会有意想不到的效果

 

 

 

版权声明:著作权归作者所有。

X

欢迎加群学习交流

联系我们