我在过去写过关于有限状态机的文章,我提到了XState。在这篇文章中,我想介绍一下这个流行的JavaScript库。
有限状态机是一种有趣的方式,可以解决复杂的状态和状态变化,并尽可能地保持你的代码没有bug。
就像我们在构建一个软件项目之前,使用各种工具对其进行建模以帮助我们进行设计,在构建一个用户界面之前,我们使用模拟图和用户体验工具对其进行思考一样,有限状态机可以帮助我们解决状态转换。
计算机程序都是在一个输入后从一个状态过渡到另一个状态。如果你不注意的话,事情就会失去控制,而XState是一个非常有用的工具,可以帮助我们在状态的复杂性增长时进行导航。
你用npm安装XState。
然后你可以用ES模块的语法在你的程序中导入它。作为最低限度,你通常会导入Machine 和interpret 函数。
import { Machine, interpret } from 'xstate'
在浏览器中,你也可以直接从CDN导入它。
<script src="https://unpkg.com/xstate@4/dist/xstate.js"></script>
并且这将在window 对象上产生一个全局XState变量。
接下来你可以使用Machine 工厂函数定义一个有限状态机。这个函数接受一个配置对象,并返回一个对新创建的状态机的引用。
const machine = Machine({
})
在配置中,我们传递一个id 字符串,用来标识状态机,即初始状态字符串。下面是一个简单的交通灯例子。
const machine = Machine({
id: 'trafficlights',
initial: 'green'
})
我们还传递一个states 对象,包含允许的状态。
const machine = Machine({
id: 'trafficlights',
initial: 'green',
states: {
green: {
},
yellow: {
},
red: {
}
}
})
这里我定义了3个状态:green yellow 和red 。
为了从一个状态过渡到另一个状态,我们将向机器发送一个消息,它将根据我们设定的配置知道该怎么做。
在这里,当我们处于green 状态时,我们设置为切换到yellow 状态,并且得到一个TIMER 事件。
const machine = Machine({
id: 'trafficlights',
initial: 'green',
states: {
green: {
on: {
TIMER: 'yellow'
}
},
yellow: {
},
red: {
}
}
})
我把它称为TIMER ,因为交通灯通常有一个简单的计时器,每隔X秒改变一次灯的状态。
现在让我们来填充另外2个状态的转换:我们从黄色到红色,以及从红色到绿色。
const machine = Machine({
id: 'trafficlights',
initial: 'green',
states: {
green: {
on: {
TIMER: 'yellow'
}
},
yellow: {
on: {
TIMER: 'red'
}
},
red: {
on: {
TIMER: 'green'
}
}
}
})
我们如何触发一个过渡?
你可以用得到机器的初始状态字符串表示。
machine.initialState.value //'green' in our case
并且我们可以使用machine (由Machine() 返回的状态机实例)的transition() 方法切换到一个新的状态。
const currentState = machine.initialState.value
const newState = machine.transition(currentState, 'TIMER')
你可以把新的状态对象存储到一个变量中,你可以通过访问value 属性得到它的字符串表示。
const currentState = machine.initialState.value
const newState = machine.transition(currentState, 'TIMER')
console.log(newState.value)
使用transition() 方法,你总是要跟踪当前的状态,这在我看来会造成一些麻烦。如果我们能向机器询问它的当前状态,那就太好了。
这可以通过创建一个状态图来实现,在XState中它被称为服务。我们这样做是调用我们从xstate 中导入的interpret() 方法,把状态机对象传给它,然后调用start() 来启动服务。
const toggleService = interpret(machine).start()
现在我们可以使用这个服务send() 方法来检索新的状态,而不必像我们在machine.transition() 中那样传递当前状态。
const toggleService = interpret(machine).start()
toggleService.send('TOGGLE')
我们可以存储返回值,它将保存新的状态。
const newState = toggleService.send('TOGGLE')
console.log(newState.value)
这仅仅是XState的表面现象。
给定一个状态,你可以通过它的nextEvents 属性知道什么会触发一个状态的改变,这个属性会返回一个数组。
是的,因为从一个状态你可以进入多个状态,这取决于你得到的触发器。
在交通灯的例子中,这不是会发生的事情,但是让我们来模拟我们在有限状态机帖子中的房子灯的例子。

当你进入房子时,你可以按下你的两个按钮之一,p1或p2。当你按下这些按钮中的任何一个,l1灯就会打开。
想象一下,这是入口处的灯,你可以把你的外套脱下来。一旦你完成了,你决定你想进入哪个房间(例如厨房或卧室)。
如果你按下按钮p1,l1会关闭,l2会打开。相反,如果你按下按钮p2,l1会关闭,l3会打开。
再按一次这两个按钮中的任何一个,p1或p2,当前开启的灯就会关闭,我们就会回到系统的初始状态。
这里是我们的XState机器对象。
const machine = Machine({
id: 'roomlights',
initial: 'nolights',
states: {
nolights: {
on: {
p1: 'l1',
p2: 'l1'
}
},
l1: {
on: {
p1: 'l2',
p2: 'l3'
}
},
l2: {
on: {
p1: 'nolights',
p2: 'nolights'
}
},
l3: {
on: {
p1: 'nolights',
p2: 'nolights'
}
},
}
})
现在我们可以创建一个服务并向它发送消息。
const toggleService = interpret(machine).start();
toggleService.send('p1').value //'l1'
toggleService.send('p1').value //'l2'
toggleService.send('p1').value //'nolights'
我们在这里错过的一件事是,当我们切换到一个新的状态时,我们如何做一些事情。这是通过行动来完成的,我们在第二个对象参数中定义了这些行动,并传递给Machine() 工厂函数。
const machine = Machine({
id: 'roomlights',
initial: 'nolights',
states: {
nolights: {
on: {
p1: {
target: 'l1',
actions: 'turnOnL1'
},
p2: {
target: 'l1',
actions: 'turnOnL1'
}
}
},
l1: {
on: {
p1: {
target: 'l2',
actions: 'turnOnL2'
},
p2: {
target: 'l3',
actions: 'turnOnL3'
}
}
},
l2: {
on: {
p1: {
target: 'nolights',
actions: ['turnOffAll']
},
p2: {
target: 'nolights',
actions: ['turnOffAll']
}
}
},
l3: {
on: {
p1: {
target: 'nolights',
actions: 'turnOffAll'
},
p2: {
target: 'nolights',
actions: 'turnOffAll'
}
}
},
}
}, {
actions: {
turnOnL1: (context, event) => {
console.log('turnOnL1')
},
turnOnL2: (context, event) => {
console.log('turnOnL2')
},
turnOnL3: (context, event) => {
console.log('turnOnL3')
},
turnOffAll: (context, event) => {
console.log('turnOffAll')
}
}
})
请看,现在传递给on 的对象中定义的每个状态转换不再是一个字符串,而是一个带有target 属性的对象(我们在这里传递我们之前使用的字符串),我们还有一个actions 属性,我们可以在这里设置要运行的动作。
我们可以通过传递一个字符串数组而不是一个字符串来运行多个动作。
你也可以直接在actions 属性上定义动作,而不是将它们 "集中 "在一个单独的对象中。
const machine = Machine({
id: 'roomlights',
initial: 'nolights',
states: {
nolights: {
on: {
p1: {
target: 'l1',
actions: (context, event) => {
console.log('turnOnL1')
},
...
但在这种情况下,把它们放在一起是很方便的,因为类似的动作是由不同的状态转换触发的。
本教程到此为止。我建议你去看看XState文档,了解XState的更多高级用法,但这只是一个开始。