这是用户在 2024-4-29 23:58 为 https://angularindepth.com/posts/1488/state-machines-in-javascript-with-xstate 保存的双语快照页面,由 沉浸式翻译 提供双语支持。了解如何保存?

​State Machines in JavaScript with XState
使用XState实现JavaScript中的状态机

In this article, we will learn about State Machines in Javascript with XState.
在本文中,我们将学习JavaScript中的状态机和XState。

State machines are models that govern a finite number of states and the events that transition from one state to the other. They are abstractions that allows us to explicitly define our application's path instead of guarding against a million possible paths.
状态机是管理有限数量的状态和从一个状态转换到另一个状态的事件的模型。它们是抽象的,允许我们显式地定义应用程序的路径,而不是防范一百万条可能的路径。

A state machine for traffic lights, for instance, would hold three states: red, yellow, and green. Green transitions to yellow, yellow to red, and red back to green. Having a state machine define our logic makes it impossible to have an illegal transition from red to yellow or yellow to green.
例如,交通灯的状态机将保持三种状态:红色、黄色和绿色。绿色转变为黄色,黄色转变为红色,红色转变为绿色。让状态机定义我们的逻辑使得不可能从红色到黄色或从黄色到绿色的非法转换。

To see how state machines can vastly reduce the complexity of our UI, business logic, and code, we’ll take the following example:
为了了解状态机如何极大地降低 UI、业务逻辑和代码的复杂性,我们将采用以下示例:

We have a light bulb that can be either lit, unlit, or broken and three buttons that can turn the bulb on or off, or break it, as represented by the following HTML code:
我们有一个灯泡,可以是 litunlitbroken ,还有三个按钮,可以打开或关闭灯泡,或者打破它,如下面的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>

​We will reference our button elements and add click event listeners to implement our business logic.
我们将引用我们的button元素并添加click事件侦听器来实现我们的业务逻辑。

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"
})

​If the light bulb is broken, turning it on again should be an impossible state transition. But the implementation above allows us to do that by simply clicking the button. So we need to guard against the transition from the broken state to the lit state. To do that, we often resort to boolean flags or verbose checks before each action, as such:
​如果灯泡坏了,再次打开它应该是不可能的状态转换。但上面的实现允许我们只需单击按钮即可做到这一点。所以我们需要防范从破碎状态到点亮状态的转变。为此,我们经常在每个操作之前使用布尔标志或详细检查,如下所示:

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
})

​But these checks are often the cause of undefined behavior in our apps: forms that submit unsuccessfully yet show a success message, or a query that fires when it shouldn’t. Writing boolean flags is extremely error-prone and not easy to read and reason about. Not to mention that our logic is now spread across multiple scopes and functions. Refactoring this behavior, would mean refactoring multiple functions or files, making it more error-prone.
但这些检查通常是我们的应用程序中出现未定义行为的原因:提交失败但显示成功消息的表单,或者不应该触发的查询。编写布尔标志非常容易出错,并且不容易阅读和推理。更不用说我们的逻辑现在分布在多个范围和函数中。重构此行为意味着重构多个函数或文件,从而更容易出错。

​This is where state machines fit perfectly. By defining your app’s behavior upfront, your UI becomes a mere reflection of your app’s logic. So let’s refactor our initial implementation to use state machines.
这就是状态机的完美之处。通过预先定义应用程序的行为,您的UI将成为应用程序逻辑的简单反映。因此,让我们重构初始实现以使用状态机。

​States and events 状态和事件

​We will define our machine as a simple object, that describes possible states and their transitions. Many state machine libraries resort to more complex objects to support more features.
我们将机器定义为一个简单的对象,它描述了可能的状态及其转换。许多状态机库采用更复杂的对象来支持更多的特性。

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

​Here’s a visualization of our state machine:
下面是我们的状态机的可视化:

Our machine object comprises of an initial property to set the initial state when we first open our app, and a states objects holding every possible state our app can be in. These states in turn have an optional on object spelling out the events  that they react to. The lit state reacts to an OFF and BREAK events. Meaning that when the light bulb is lit, we can either turn it off or break it. We cannot turn it on while it is on (although we can model our logic that way if we choose to.)
我们的机器对象包含一个 initial 属性,用于在我们首次打开应用程序时设置初始状态,以及一个 states 对象,用于保存我们的应用程序可能处于的所有可能状态。这些 states 又具有一个可选的 on 对象拼写出他们反应的 eventslit 状态对 OFFBREAK 事件做出反应。这意味着当灯泡点亮时,我们可以将其关闭或打破。我们无法在它打开时将其打开(尽管如果我们选择的话,我们可以以这种方式建模我们的逻辑。)

​Final States ​最终状态

The broken state does not react to any event which makes it a final state — a state that does not transition to other states, ending the flow of the whole state machine.
broken 状态不会对任何事件做出反应,从而使其成为最终状态 - 该状态不会转换到其他状态,从而结束整个状态机的流程。

Transitions 过渡

​A transition is a pure function that returns a new state based on the current state and an event.
transition是一个纯函数,它基于当前状态和事件返回一个新状态。

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

​The transition function traverses our machine’s states and their events:
transition函数遍历我们机器的状态和它们的事件:

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)

​If we are not handling a specific event in a given state, our state is going to remain unchanged, returning the old state. This is a key idea behind state machines.
如果我们不在给定状态下处理特定事件,我们的状态将保持不变,返回旧状态。这是状态机背后的关键思想。

​Tracking and sending events
​跟踪和发送事件

To track and send events, we will define a state variable that defaults to our machine’s initial state and a send function that takes an event and updates our state according to our machine’s implementation.
为了跟踪和发送事件,我们将定义一个 state 变量,它默认为我们机器的初始状态,以及一个send函数,它接受一个事件并根据我们机器的实现更新我们的状态。

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

So, now instead of keeping up with our state through booleans and if statements, we simply dispatch our events and call for our UI to update:
所以,现在我们不再通过布尔值和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
})

Our three buttons will work as intended, and whenever we break our light bulb, the user will not be able to turn it on or off.
我们的三个按钮将按预期工作,每当我们打破我们的灯泡,用户将无法打开或关闭它。

State machines using XState
使用XState的状态机

XState is a state management library that popularized the use of state machines on the web in recent years. It comes with tools to create, interpret, and subscribe to state machines, guard and delay events, handle extended state, and many other features.
XState是一个状态管理库,近年来在Web上普及了状态机的使用。它附带了创建、解释和订阅状态机、保护和延迟事件、处理扩展状态以及许多其他功能的工具。

To install XState, run npm install xstate.
要安装XState,请运行 npm install xstate

XState provides us with two functions to create and manage (track, send events, etc.) our machines: createMachine and interpret.
XState为我们提供了两个函数来创建和管理(跟踪、发送事件等)。我们的机器: createMachineinterpret

import { createMachine, interpret } from "xstate"
 
const machine = createMachine({
  initial: "lit",
  states: {
    lit: {
      on: {
        OFF: "unlit",
        BREAK: "broken",
      },
    },
    unlit: {
      on: {
        ON: "lit",
        BREAK: "broken",
      },
    },
    broken: {},
  },
})
 
const service = interpret(machine)
service.start()
 
turnBulbOn.addEventListener("click", () => {
  service.send("ON")
  lightbulb.innerText = service.state.value
})
 
turnBulbOff.addEventListener("click", () => {
  service.send("OFF")
  lightbulb.innerText = service.state.value
})
 
breakBulb.addEventListener("click", () => {
  service.send("BREAK")
  lightbulb.innerText = service.state.value
})

We can minimize our code further by subscribing to our state and updating the UI as a subscription:
我们可以通过订阅我们的状态并将UI更新为订阅来进一步减少代码:

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

You notice that we’re now using state.value instead of state, because XState exposes a number of useful methods and properties on the state object, one of which is state.matches:
您注意到我们现在使用 state.value 而不是 state ,因为XState在 state 对象上公开了许多有用的方法和属性,其中之一是 state.matches

state.matches('lit') // true or false based on the current state
state.matches('non-existent-state') // false

One other useful state method is the state.can method, which returns true or false based on whether the current state handles a given event or not.
另一个有用的state方法是 state.can 方法,它根据当前状态是否处理给定事件返回 truefalse

Thus, we can initially hide our Turn on button and show/hide our buttons based on whether we can dispatch their related events:
因此,我们最初可以隐藏 Turn on 按钮,并根据是否可以分派相关事件来显示/隐藏按钮:

<p>The lightbulb is <span id="lightbulb">lit</span></p>
 
<!-- hidden on page load -->
<button hidden id="turn-on">Turn on</button>
 
<button id="turn-off">Turn off</button>
<button id="break">Break</button>
 
<!-- reloads the page -->
<button hidden id="reset" onclick="history.go(0)">Reset</button>
const lightbulb = document.getElementById("lightbulb")
const turnBulbOn = document.getElementById("turn-on")
const turnBulbOff = document.getElementById("turn-off")
const breakBulb = document.getElementById("break")
const reset = document.getElementById("reset")
 
service.subscribe((state) => {
  lightbulb.innerText = state.value
 
  turnBulbOn.hidden = !state.can("ON")
  turnBulbOff.hidden = !state.can("OFF")
  breakBulb.hidden = !state.can("BREAK")
 
  reset.hidden = !state.matches("broken")
})

So now, whenever our state changes, we can show and hide the appropriate buttons.
所以现在,每当我们的状态发生变化时,我们都可以显示和隐藏适当的按钮。

Actions and side effects
作用和副作用

To run side effects inside our machine, XState has three types of actions: transition actions that run on an event, entry actions that run when we enter a state, and exit actions that run when we exist a state. entry, exit, and actions can all be an array of functions (or even string references as we will see.)
为了在我们的机器内运行副作用,XState 具有三种类型的操作:在事件上运行的转换操作、在进入状态时运行的 entry 操作以及在存在状态时运行的 exit 操作。 entryexitactions 都可以是函数数组(甚至是我们将看到的字符串引用。)

const machine = createMachine({
  initial: "open",
  states: {
    open: {
      entry: () => console.log("entering open..."),
      exit: () => console.log("exiting open..."),
      on: {
        TOGGLE: {
          target: "close",
          actions: () => console.log("toggling..."),
        },
      },
    },
    close: {},
  },
})

Context and extended state
上下文和扩展状态

When talking about state machines, we can distinguish between two types of states: finite state and extended state.
当谈到状态机时,我们可以区分两种类型的状态:有限状态和扩展状态。

A person, for instance, can be either standing or sitting. They cannot be standing and sitting at the same time. They also can be either awake or asleep, and an array of other finite states. By contrast, a person can also have state that’s potentially infinite. E.g., their age, nicknames, or hobbies. This is called infinite or extended state. It helps to think about finite state as qualitative state while extended state is quantitative.
例如,一个人可以站着或坐着。他们不能同时站着和坐着。它们也可以是醒着的或睡着的,以及一系列其他有限状态。相比之下,一个人也可以有潜在无限的状态。例如,他们的年龄昵称或者爱好这被称为无限状态或扩展状态。它有助于将有限状态视为定性状态,而扩展状态是定量的。

In our example, we will track how many times we switch our light bulb (as extended state) and display it in our message.
在我们的示例中,我们将跟踪我们开关灯泡的次数(作为扩展状态),并将其显示在我们的消息中。

import { assign, createMachine, interpret } from "xstate"
 
const machine = createMachine({
  initial: "lit",
  context: { switchCount: 0 },
  states: {
    lit: {
      entry: "switched",
      on: {
        OFF: "unlit",
        BREAK: "broken",
      },
    },
    unlit: {
      entry: "switched",
      on: {
        ON: "lit",
        BREAK: "broken",
      },
    },
    broken: {},
  },
}).withConfig({
  actions: {
    switched: assign({ switchCount: (context) => context.switchCount + 1 }),
  },
})
 
const service = interpret(machine)
service.start()
 
service.subscribe((state) => {
  lightbulb.innerText = `${state.value} (${state.context.switchCount})`
 
  turnBulbOn.hidden = !state.can("ON")
  turnBulbOff.hidden = !state.can("OFF")
  breakBulb.hidden = !state.can("BREAK")
 
  reset.hidden = !state.matches("broken")
})

We now have a context property that holds a switchCount initiated with 0. And as mentioned before, we added entry actions to our lit and unlit states using string references and defined our functions using the withConfig method on our machine to eliminate any code repetition.
我们现在有一个 context 属性,它包含一个由 0 发起的 switchCount 。如前所述,我们使用字符串引用将 entry 操作添加到 litunlit 状态,并在机器上使用 withConfig 方法定义函数以消除任何代码重复。

To update our context inside the machine, XState provides an assign function, which gets called to form an action and cannot be used within a function (e.g., actions: () => { assign(...) }).
为了更新机器内部的上下文,XState提供了一个 assign 函数,该函数被调用以形成一个动作,并且不能在函数中使用(例如, actions: () => { assign(...) } )。

Also, notice that despite our default value of 0 for our switchCount, XState will run our entry action when our service starts, displaying a count of 1 in our UI.
另外,请注意,尽管 switchCount 的默认值为 0 ,但 XState 仍会在服务启动时运行 entry 操作,并在 UI 中显示 1 的计数。

Guards 卫兵

Guards allow us to block a transition from happening given a condition (such as the outcome of an input validation.) Unlike actions, guards only apply to events, but they enjoy the same flexibility in their definition.
守卫允许我们在给定条件(例如输入验证的结果)的情况下阻止转换的发生。与操作不同,守卫仅适用于事件,但它们在定义上具有相同的灵活性。

In our example, we will block any attempt to break a light bulb if the switch count exceeds 3.
在我们的示例中,如果开关数量超过 3 ,我们将阻止任何破坏灯泡的尝试。

const machine = createMachine({
  initial: "lit",
  context: { switchCount: 0 },
  states: {
    lit: {
      entry: "switched",
      on: {
        OFF: "unlit",
        BREAK: { target: "broken", cond: "goodLightBulb" },
      },
    },
    unlit: {
      entry: "switched",
      on: {
        ON: "lit",
        BREAK: { target: "broken", cond: "goodLightBulb" },
      },
    },
    broken: {},
  },
}).withConfig({
  actions: {
    switched: assign({ switchCount: (context) => context.switchCount + 1 }),
  },
  guards: {
    goodLightBulb: (context) => context.switchCount <= 3,
  },
})

We add guards in our events using the weirdly named cond property (stands for condition), and we use the withConfig guards property to write our definition for the goodLightBulb guard.
我们在事件中使用奇怪命名的 cond 属性(代表条件)添加守卫,并使用 withConfig guards 属性为 goodLightBulb 守卫编写定义。

The service.can we used earlier runs our guards to determine whether an event is possible to dispatch or not, which means that our UI will correctly remove the break button once our condition is met. If our guard function fails to run, service.can​ will return false​.
我们之前使用的 service.can 运行我们的守卫来确定事件是否可以调度,这意味着一旦我们的条件得到满足,我们的UI将正确地删除 break 按钮。如果guard函数运行失败, service.can 将返回 false

Eventless transitions 无事件过渡

Let’s say that no matter how good our light bulb is, if the switch count reaches 10, it should break.
假设无论我们的灯泡有多好,如果开关数量达到 10 ,它就应该坏掉。

To achieve that, we can use an eventless transtion using the always property with a condition:
为了实现这一点,我们可以使用带有条件的 always 属性来使用无事件转换:

const machine = createMachine({
  initial: "lit",
  context: { switchCount: 0 },
  states: {
    lit: { /*...*/ },
    unlit: {
      entry: "switched",
      on: { /*...*/ },
      always: {
        cond: (context) => context.switchCount >= 10,
        target: "broken",
      },
    },
    broken: {},
  },
})

What’s next? 下一步是什么?

Despite covering the use of state machines to model our UI, state machines can be and are used everywhere. From handling data-fetching, loading, and error states to building complex interactive animations.
尽管讨论了使用状态机来建模我们的UI,但状态机可以在任何地方使用。从处理数据获取、加载和错误状态到构建复杂的交互式动画。

In this article, we covered key concepts behind state machines, events, and transitions, and we implemented a state machine in XState, making use of extended state, actions, guarded and eventless transitions. And while we updated our UI manually by subscribing to our machine’s service, XState supports almost all major frontend frameworks.
在本文中,我们介绍了状态机、事件和转换背后的关键概念,并在XState中实现了一个状态机,使用了扩展的状态、操作、受保护的和无事件的转换。虽然我们通过订阅机器的服务来手动更新UI,但XState支持几乎所有主要的前端框架。