最近看到一篇状态机的文章,出于好奇,抱着多了解一些相关知识的心态多找了几篇文章,对于高大上的概念我其实是不太感冒的,作为技术人,我还是比较看重实际代码,结果找了几篇文章,都在说概念,提到代码,要么只有大概的伪代码要么就是没头没尾的片段,最好的就是给个跟实际场景差得有点远的 demo
,这我就有点纳闷了,既然这东西那么好,你倒是 show me the code
啊
没有完整的代码,光看概念,我实在是难以评判这个东西的落地体验到底如何,所以就自己花费了一些时间看了官方文档,然后找了个实际场景的小例子,分别写了两版代码,一版是不用状态机,一版是用状态机,有了代码,才好进行一线技术人的评判
下面是两版实现的效果,左边是正常版本,右边是 XState
版本
两版完整可执行代码已经上传到 github 了,有兴趣的可以看下
最终实现效果如上图,就是一个实际工作场景中可以遇到的表格数据展示,表格有分页功能,可以进行搜索,翻页或搜索的时候,会请求远程接口展示loading
状态,接口数据返回渲染到表格里
本文两版实现都基于
vue3.x
正常版本
先看下正常是如何实现这个功能的
一些需要展示在页面上的状态
const data = reactive({
// 是否处于加载中
loading: false,
// 页码
page: 1,
// 总数
total: 0,
// 接口返回的表格数据
list: [] as TData['list']
// 搜索字符串
searchValue: '',
// 表格组件的 columns
columns,
})
最主要的是数据刷新方法
const methods = {
async freshData() {
data.loading = true
try {
const respData = await getTableData(data.page, data.searchValue)
data.total = respData.total
data.list = respData.list
} catch(e) {
data.list = []
Message.error('获取数据出错')
}
data.loading = false
}
}
虽然有一些状态的处理逻辑,但结合写起来问题不大,再配合上组件库,其实是感受到什么复杂度的,当然了,根据业务复杂度的不同,这里代码量可能会有所增减,但不管你业务再怎么复杂,本质上的状态还是那几个
状态机(XState)版本
js
版本的状态机管理库我这里选择 XState
首先,划分好状态,关于这个模块存在:空闲、请求数据中、请求数据成功、请求数据失败这四个状态,且这四个状态之间存在相互的转化关系,用JSON
伪代码描述为:
{
"initial": "idle",
"states": {
"idle": {
"on": {
"LOAD": "pending"
}
},
"pending": {
"on": {
"success": "success",
"failure": "failure",
}
},
"success": {
"on": {
"LOAD": "pending"
}},
"failure": {
"on": {
"LOAD": "pending"
}
}
},
}
开始写代码
首先要创建一个状态机,我们遵从 XState
的最佳规范,从一个 Model 来创建状态机
const model = createModel({
loading: false,
page: 1,
total: 0
list: [],
searchValue: '',
}, {
events: {
LOAD: (params: { page: number, searchValue: string }) => params
}
})
createModel
的第一个参数就是状态机的 context,有了这个东西我们才能将实际要解决的问题与状态机结合起来,这里的 context
数据和上面一版是一样的,只不过我们在这里将它们放到状态机的 context
里去管理了
第二个参数是用于发送请求数据的 action
,action
的执行会导致状态的变化
然后从 model
创建状态机,这个状态机有四种状态,初始状态是空闲状态
const machine = model.createMachine({
context: model.initialContext,
initial: 'idle',
states: {
idle: {},
pending: {},
success: {},
failure: {},
}
})
如果状态机处于 idle
状态,那么将到加载数据的 action
的时候,就应当执行这个动作,且会将状态机的状态转变为 pending
态
const machine = model.createMachine({
// ...
states: {
idle: {
on: {
LOAD: {
target: 'pending',
actions: model.assign({
loading: () => true,
page: (_, event) => event.page,
searchValue: (_, event) => event.searchValue
})
}
}
},
// ...
}
})
在这一步,我们在 action
执行的时候,改变了 context
内的数据,以响应页面状态的变化
pending
是用于获取接口数据的状态,应当是一个 promise
的状态机,根据 XState
的最佳实践,这里将其作为一个 Service
const machine = model.createMachine({
// ...
states: {
pending: {
invoke: {
id: 'pending-data',
// getTableData 是请求接口获取数据的一个 promise 方法
src: context => getTableData(context.page, context.searchValue),
onDone: {},
onError: {}
}
},
// ...
}
})
当转变到 pending
状态时,会调用 src
指向的服务,我们这里是一个 promise
方法,当这个 promise
方法 resolve
的时候,会执行 onDone
方法,当这个 promise
方法 reject
的时候,会执行 onError
方法
当 onDone
的时候,用 actions
将请求回来的数据更新到 context
,同时将状态机的状态转为 success
onDone: {
target: 'success',
actions: model.assign({
loading: () => false,
total: (_, event) => ((event as any).data as TData).total,
list: (_, event) => ((event as any).data as TData).list
})
},
当 onError
的时候,同样更新 context
,并将状态机的状态转为 failure
,同时还可以报个 message
错误
onError: {
target: 'failure',
actions: [
model.assign({
loading: false,
list: []
}),
() => Message.error('获取数据出错')
]
}
在我们这个例子,状态机进入 success
和 failure
后,所能转换的下一个状态以及接受的 action
都是相同的
success: {
on: {
LOAD: {
target: 'pending',
actions: actions: model.assign({
loading: () => true,
page: (_, event) => event.page,
searchValue: (_, event) => event.searchValue
})
}
}
},
failure: {
on: {
LOAD: {
target: 'pending',
actions: actions: model.assign({
loading: () => true,
page: (_, event) => event.page,
searchValue: (_, event) => event.searchValue
})
}
}
}
对状态机进行可视化如下:
状态机定义好了,下一步就是使用了
import { useMachine } from '@xstate/vue'
export default defineComponent({
components: {
Table,
Search: Input.Search
},
setup() {
const { state, send } = useMachine(machine)
send('LOAD', { page: state.value.context.page, searchValue: state.value.context.searchValue })
const methods = {
handleSearch(v: string) {
send('LOAD', { page: 1, searchValue: v })
},
handlePageChange(page: number) {
send('LOAD', { page: page, searchValue: state.value.context.searchValue })
}
}
return {
state,
columns,
...methods
}
}
})
使用 @xstate/vue 提供的 useMachine
方法,可以拿到 state
和 action
和send
,通过 state.context
可以获得在状态机里定义的 context
数据,通过 send
可以发送 action
来进行状态的转变
总结
代码行数
两版实现的代码行数分别为 77
行(正常版)、134
行(XState
版),哪怕是抛开样板代码来看,XState
版本的代码行数也比正常版要多(最起码不少)
实现体验
XState
版本更像是正常版本的 声明式
实现,其内敛了状态的变化逻辑,使得代码的组织和状态的变化更加有序,但同时也明显地增加了开发者的心智负担
从技术极致的角度看,我当然推荐你使用状态机来组织状态,但我们大部分人写的代码都是为业务所服务的,从权衡的角度来看,这种编写代码的方式或许不仅不能提升代码可维护性,反而会是一个拖累
首先,状态机的概念很多,我几乎花费了两天的时间才把文档看完,且相关的教程和文章相比于其他炙手可热的技术来说少得可怜(甚至比不上 rxjs
),这就导致学习门槛较高,就算你给团队成员学习的时间,也难以保证大家都能学得到位,在没有充分理解的情况下很容易编写不符合理念的代码,代码都没写对,怎么指望能发挥应有的效果?
其次,业务千变万化,这就要求写的代码也要足够灵活,甚至有些时候要写一些反逻辑的 hack
代码,if...else
再多再难看,最起码你明确地知道无论再怎么糟糕肯定能继续堆下去,但如果一上来就被状态机的条条框框给限死了,改起来痛苦就不说了,万一改都改不了那才叫大麻烦,以我多年从业经验来看,我实在想不到我所遇到的哪些实际场景用状态机来解决会明显更好
当然,我不是说这个东西不好,存在即合理,这个东西必然是以解决相应问题的目的而出现的,它必然是有可以大展拳脚的场景,但不应该认为这是万能灵药,要用辩证的眼光来看待问题