近年来因为一些前端框架的兴起然后逐渐成熟,组件化的概念已经深刻人心,为了管理好大型应用中错综复杂的组件,又有了单向数据流的思想指引着咱们,Vuex、Redux、MobX等状态管理工具也许你们都信手拈来。
咱们手握着这些工具,不断思考着哪些数据应该放在全局,哪些数据应该局部消化,这样那样的数据该怎样流转。仔细想一想会发现,咱们一直在作的是如何将数据存在合理的地方,进而去规范怎样使用这些数据,咱们称之为状态管理,但我以为好像只是作了状态存储与使用,却没有作好管理二字。嗯,总感受差了些什么。javascript
来看一段简单的代码:html
state = { data: [ { id: 1, userName: xxx }, { id: 2, userName: yyy } ] }
若是根据UI = f(state)来讲,上面的state.data就是一个状态,它能直接反映视图的样子:前端
render () { const {data} = this.state return ( <div> { data && data.length ? data.map(item => <div :key={item.id}>{item.userName}</div>) : '暂无数据' } </div> ) }
咱们还会在合适的时机进行某种操做去更新状态,好比请求获取数据的接口就会去更新上面的data:vue
updateData () { getData().then(({data}) => { this.setState({data}) }) }
但随着时间的推移,这样的状态会愈来愈多,更新状态的方法暗藏在日益膨胀的代码中,维护者可能本身都要如履薄冰地一翻抽丝剥茧才勉强捋清楚状态什么时机更新,为何要更新,更别说若是是去接盘一份祖传代码了。
究其缘由,我以为是没有将状态描述清楚,更别说管理好状态。因此一个描述得清楚的状态是长什么样子呢?好比:开始的时候,data是空数组,初始化完成须要去更新data,增删改完成后都须要更新data。
想一想平常,有没有地方能一眼就看清楚这些状态信息?需求文档?UI稿?靠谱吗?java
本身动手丰衣足食,咱们的目标是在代码里就能清晰地看到这些状态信息。若是咱们可以写一份配置文件来将它们描述清楚,而后写代码的时候就根据这份配置文件来写,有修改的时候也必须先修改这份配置文件,那咱们最后看配置文件就能对状态信息一目了然了。
为了达到这样的目标,咱们得请有限状态机来帮忙。概念性的东西请移步到JavaScript与有限状态机,总的来讲,有限状态机是一个模型,它能描述清楚有哪些状态,状态之间是怎样转化的,它有如下特色:
1.状态的数量是固定的
2.状态会由于触发了某种行为而转变成另外一种状态(好比典型的promise,初始状态为pending,resolve后状态转变成fulfilled,reject则变成rejected)
3.任意时间点状态惟一(初始化完成了才能进行增删改嘛)
ok,了解这些以后,咱们来看看怎样一步步达到目的。
咱们以一个需求为例:
就是一个没有一毛钱特效的Todoist,很是简单的增删改查。react
按照以前的想法,咱们首先须要一份配置文件来描述状态:git
const machine = { // 初始状态 initial: "start", start: { INIT: "loadList" }, loadList: { LOAD_LIST_SUCCESS: "showList", LOAD_LIST_ERROR: "showListError" }, showListError: { RETRY: "loadList" }, showList: { ADD: "add", EDIT: "edit", DELETE: "delete" }, edit: { SAVE_EDIT: "saveEdit" }, saveEdit: { SAVE_EDIT_SUCCESS: "loadList" }, delete: { DELETE_SUCCESS: "loadList" }, add: { ADD_SUCCESS: "loadList" } };
配置是写完了,如今对着上面的需求gif图说一下这份配置是什么意思。github
剩下的配置就不继续写了,能够看到经过这份配置,咱们能够清晰知道,这份代码究竟作了些什么,并且写这份配置有利于整理好本身的思路,让本身首先将需求过一遍,将全部边边角角经过写配置预演一遍,而不是拿到需求就开撸,遇到了问题才发现以前写的代码不适用。同理若是需求有变,首先从这份配置入手,看看这波修改会对哪些状态分支形成影响,就不会出现那种不知道改一个地方会不会影响到别的地方宛如拆炸弹同样的心情。
接着,为了方便根据这份配置来进行操做,须要实现一点辅助函数:数组
class App extends Component { constructor(props) { state = { curState: machine.initial } } handleNextState (nextState, action) { switch (nextState) { case "loadList": // 处理loadList的逻辑 break; } } transition (action) { const { curState } = this.state; const nextState = machine[curState][action.type]; if (nextState) { this.setState({ curState: nextState }, () => this.handleNextState(nextState, action) ); } } }
基本就是这样的结构,经过this.transition({ type: "INIT" })触发一个事件(INIT)将当前状态(start)转变成另一个状态(loadList),而handleNextState则处理状态转变后的逻辑(当状态变成loadList须要去请求接口获取列表数据)。经过这样的方式,咱们真正将状态管理了起来,由于咱们有清晰的配置文件去描述状态,咱们有分层清晰的地方去处理当前状态须要处理的逻辑,这就至关于有明确的战略图,你们都根据这份战略图各司其职作好本身的本分,这不是将状态管理得层次分明吗?
并且这样作以后,比较容易规避一些意外的错误,由于任意时间点状态惟一这个特色,带来了状态只能从一个状态转变到另外一个状态,好比点击一个按钮提交,这时的状态是提交中,咱们常常须要去处理用户重复点击而致使重复提交的事情:promise
let isSubmit = false const submit = () => { if (isSubmit === true) return isSubmit = true toSubmit().then(() => isSubmit = false) } submit()
使用有限状态机进行管理后就不须要写这种额外的isSubmit状态,由于提交中的状态只能转变为提交完成。
上面的代码完整版请点这里。
虽然第一版对于状态的管理更加清晰了一些,但仍然不够直观,若是能将配置转化成图就行了,有图有真相嘛。心想事成:
不只有图可看,还能够逼真地将全部状态都预演一遍。这个好东西就是xstate给予咱们的,它是一个实现有限状态机模型的js库,感兴趣能够去详看,这里咱们只须要按照它的写法去写状态机的配置,就能够生成出这样的图。
看过xstate会发现,里面的东西真很多,其实若是只是想在简单的项目上用这种模式试试水,却要把整个库引进来彷佛不太划算。那,不如本身来撸一个简化版?
心动不如行动,先分析一下第一版有什么不足之处。
如今首要的任务就是把有限状态机的模式抽离出来,顺便使用xstate的写法来写配置。
const is = (type, val) => Object.prototype.toString.call(val) === "[object " + type + "]"; export class Fsm { constructor(stateConfig) { // 状态描述配置 this.stateConfig = stateConfig; // 当前状态 this.state = stateConfig.initial; // 上一个状态 this.lastState = ""; // 状态离开回调集合 this.onExitMap = {}; // 状态进入回调集合 this.onEntryMap = {}; // 状态改变回调 this.handleStateChange = null; } /** * 改变状态 * @param type 行为类型 描述当前状态经过该类型的行为转变到另外一个状态 * @param arg 转变过程当中的额外传参 * @returns {Promise<void>} */ transition({ type, ...arg }) { const states = this.stateConfig.states; const curState = this.state; if (!states) { throw "states undefined"; } if (!is("Object", states)) { throw "states should be object"; } if ( !states[curState] || !states[curState]["on"] || !states[curState]["on"][type] ) { console.warn(`transition fail, current state is ${this.state}`); return; } const nextState = states[curState]["on"][type]; const curStateObj = states[curState]; const nextStateObj = states[nextState]; // 状态转变的经历 return ( Promise.resolve() // 状态离开 .then(() => this.handleLifeCycle({ type: "onExit", stateObj: curStateObj, arg: { exitState: curState } }) ) // 状态改变 .then(() => this.updateState({ state: nextState, lastState: curState })) // 进入新状态 .then(() => this.handleLifeCycle({ type: "onEntry", stateObj: nextStateObj, arg: { state: nextState, lastState: curState, ...arg } }) ) ); } /** * 状态改变回调 只注册一次 * @param cb */ onStateChange(cb) { cb && is("Function", cb) && !this.handleStateChange && (this.handleStateChange = cb); } /** * 注册状态离开回调 * @param type * @param cb */ onExit(type, cb) { !this.onExitMap[type] && (this.onExitMap[type] = cb); } /** * 注册状态进入回调 * @param type * @param cb */ onEntry(type, cb) { !this.onEntryMap[type] && (this.onEntryMap[type] = cb); } /** * 更新状态 * @param state * @param lastState */ updateState({ state, lastState }) { this.state = state; this.lastState = lastState; this.handleStateChange && this.handleStateChange({ state, lastState }); } /** * 处理状态转变的生命周期 * @param stateObj * @param type onExit/onEntry * @param arg * @returns {*} */ handleLifeCycle({ stateObj, type, arg }) { const cbName = stateObj[type]; if (cbName) { const cb = this[`${type}Map`][cbName]; if (cb && is("Function", cb)) { return cb(arg); } } } /** * 获取当前状态 * @returns {*} */ getState() { return this.state; } /** * 获取上一个状态 * @returns {string|*} */ getLastState() { return this.lastState; } }
而后这样使用就好:
const stateConfig = { initial: "start", states: { start: { on: { INIT: "loadList" }, onExit: "onExitStart" }, loadList: { on: { LOAD_LIST_SUCCESS: "showList", LOAD_LIST_ERROR: "showListError" }, onEntry: "onEntryLoadList" } } } /* 结果: 1.console.log('onExitStart') 2.console.log('onEntryLoadList') 3.console.log('transition success') transition以及生命周期函数onExit、onEntry都支持promise控制异步流程 */ const fsm = new Fsm(stateConfig); transition({ type: "INIT"}).then(() => { console.log('transition success') }) fsm.onExit('onExitStart', (data) => { return new Promise(resolve => { setTimeout(() => { console.log('onExitStart') resolve() }, 1000) }) }) fsm.onEntry('onEntryLoadList', (data) => { console.log('onEntryLoadList') })
总算把有限状态机抽成一个工具来使用了,已经完成了最关键的一步。
若是想在react中使用,想到比较方便的使用形式是高阶组件,须要用到有限状态机的组件传进高阶组件,就立马拥有了使用有限状态机的能力。
import React from "react"; import { Fsm } from "../fsm"; export default function(stateConfig) { const fsm = new Fsm(stateConfig); return function(Component) { return class extends React.Component { constructor() { super(); this.state = { machineState: { // 当前状态 value: stateConfig.initial, // 上一个状态 lastValue: "" } }; } updateMachineState(data) { this.setState({ machineState: { ...this.state.machineState, ...data } }); } componentDidMount() { this.handleStateChange(); this.handleEvent(); } /** * 处理状态更新 */ handleStateChange() { fsm.onStateChange(({ state, lastState }) => { this.updateMachineState({ value: state, lastValue: lastState }); }); } /** * 处理状态改变事件 */ handleEvent() { const states = stateConfig.states; // 获取状态配置中全部的onEntry与onExit const eventObj = Object.keys(states).reduce( (obj, key) => { const value = states[key]; const onEntry = value.onEntry; const onExit = value.onExit; onEntry && obj.onEntry.push(onEntry); onExit && obj.onExit.push(onExit); return obj; }, { onEntry: [], onExit: [] } ); // 获取组件实例中onEntry与onExit的回调方法 Object.keys(eventObj).forEach(key => { eventObj[key].forEach(item => { this.ref[item] && fsm[key](item, this.ref[item].bind(this.ref)); }); }); } render() { return ( <Component ref={c => (this.ref = c)} {...this.state} transition={fsm.transition.bind(fsm)} /> ); } }; }; }
使用的时候就能够:
const stateConfig = { initial: "start", states: { start: { on: { INIT: "loadList" }, onExit: "onExitStart" } } } class App extends Component { componentDidMount () { this.props.transition({ type: "INIT" }); } onExitStart () { console.log('onExitStart ') } } export default withFsm(machine)(App);
如今咱们能够愉快地使用这个高阶组件将Todoist重构一遍。
固然,大佬们会说了,个人项目比较复杂,有没有比较完善的解决方案呢?那确定是有的,能够看看react-automata,将xstate集成到react中使用。因为咱们上面的小高阶组件用法比较像react-automata,因此基本不须要什么改动,就能够迁移到react-automata,使用react-automata再重构一遍Todoist。
对于符合有限状态机的使用场景,使用它确实能将状态管理起来,由于咱们的状态不再是那种如isSubmit = false/true那样杂乱无章的状态,而是某个时间节点里的一个总括状态。无论怎样,有限状态机的方案仍是促使了咱们去从新思考怎样能更大程度地提升项目的可维护性,提供了一个新方向尽量减小祖传代码,改起bug或者需求的时候分析起来更加容易,终极目的只有一个,那就是,但愿能早点下班。