利用 XState(有限状态机) 编写易于变动的代码

目前来讲,不管是 to c 业务,仍是 to b 业务,对于前端开发者的要求愈来愈高,各类绚丽的视觉效果,复杂的业务逻辑层出不穷。针对于业务逻辑而言,贯穿后端业务和前端交互都有一个关键点 —— 状态转换。javascript

固然了,这种代码实现自己并不复杂,真正的难点在于如何快速的进行代码的修改。html

在实际开发项目的过程当中,ETC 原则,即 Easier To Change,易于变动是很是重要的。为何解耦很好? 为何单一职责颇有用? 为何好的命名很重要?由于这些设计原则让你的代码更容易发生变动。ETC 甚至能够说是其余原则的基石,能够说,咱们如今所做的一切都是为了更容易变动!!特别是针对于初创公司,更是如此。前端

例如:项目初期,当前的网页有一个模态框,能够进行编辑,模态框上有两个按钮,保存与取消。这里就涉及到模态框的显隐状态以及权限管理。随着时间的推移,需求和业务发生了改变。当前列表没法展现该项目的全部内容,在模态框中咱们不但须要编辑数据,同时须要展现数据。这时候咱们还须要管理按钮之间的联动。仅仅这些就较为复杂,更不用说涉及多个业务实体以及多角色之间的细微控制。java

从新审视自身代码,虽然以前咱们作了大量努力利用各类设计原则,可是想要快速而安全的修改散落到各个函数中的状态修改,仍是很是浪费心神的,并且还很容易出现“漏网之鱼”。react

这时候,咱们不只仅须要依靠自身经验写好代码,同时也须要一些工具的辅助。git

有限状态机

有限状态机是一个很是有用的数学计算模型,它描述了在任何给定时间只能处于一种状态的系统的行为。固然,该系统中只可以创建出一些有限的、定性的“模式”或“状态” ,并不描述与该系统相关的全部(多是无限的)数据。例如,水能够是四种状态中的一种: 固体(冰)、液体、气体或等离子体。然而,水的温度能够变化,它的测量是定量的和无限的。github

总结来讲,有限状态机的三个特征为:编程

  • 状态总数(state)是有限的。
  • 任一时刻,只处在一种状态之中。
  • 某种条件下,会从一种状态转变(transition)到另外一种状态。

在实际开发中,它还须要:后端

  • 初始状态
  • 触发状态变化的事件和转换函数
  • 最终状态的集合(有多是没有最终状态)

先看一个简单的红绿灯状态转换:设计模式

const light = {
  currentState: 'green',
  
  transition: function () {
    switch (this.currentState) {
      case "green":
        this.currentState = 'yellow'
        break;
      case "yellow":
        this.currentState = 'red'
        break;
      case "red": 
        this.currentState = 'green'
        break;
      default:
        break;
    }
  }
}

有限状态机在游戏开发中大放异彩,已经成为了一种经常使用的设计模式。用这种方式可使每个状态都是独立的代码块,与其余不一样的状态分开独立运行,这样很容易检测遗漏条件和移除非法状态,减小了耦合,提高了代码的健壮性,这么作可使得游戏的调试变得更加方便,同时也更易于增长新的功能。

对于前端开发来讲,咱们能够从其余工程领域中多年使用的经验学习与再创造。

XState 体验

实际上开发一个 简单的状态机并非特别复杂的事情,可是想要一个完善,实用性强,还具备可视化工具的状态机可不是一个简单的事。

这里我要推荐 XState,该库用于建立、解释和执行有限状态机和状态图。

简单来讲:上述的代码能够这样写。

import { Machine } from 'xstate'

const lightMachine = Machine({
  // 识别 id, SCXML id 必须惟一
  id: 'light',
  // 初始化状态,绿灯
  initial: 'green',
  
  // 状态定义 
  states: {
    green: {
      on: {
        // 事件名称,若是触发 TIMRE 事件,直接转入 yellow 状态
        TIMRE: 'yellow'
      }
    },
    yellow: {
      on: {
        TIMER: 'red'
      }
    },
    red: {
      on: {
        TIMER: 'green'
      }
    }
  }
})

// 设置当前状态
const currentState = 'green'

// 转换的结果
const nextState = lightMachine.transition(currentState, 'TIMER').value 
// => 'yellow'

// 若是传入的事件没有定义,则不会发生转换,若是是严格模式,将会抛出错误
lightMachine.transition(currentState, 'UNKNOWN').value

其中 SCXML 是状态图可扩展标记语言, XState 遵循该标准,因此须要提供 id。当前状态机也能够转换为 JSON 或 SCXML。

虽然 transition 是一个纯函数,很是好用,可是在真实环境使用状态机,咱们仍是须要更强大的功能。如:

  • 跟踪当前状态
  • 执行反作用
  • 处理延迟过分以及时间
  • 与外部服务沟通

XState 提供了 interpret 函数,

import { Machine,interpret } from 'xstate'

// 。。。 lightMachine 代码

// 状态机的实例成为 serivce
const lightService = interpret(lightMachine)
   // 当转换时候,触发的事件(包括初始状态)
  .onTransition(state => {
    // 返回是否改变,若是状态发生变化(或者 context 以及 action 后文提到),返回 true 
    console.log(state.changed) 
    console.log(state.value)
  })
  // 完成时候触发
  .onDone(() => {
    console.log('done')
  })

// 开启
lightService.start()

// 将触发事件改成 发送消息,更适合状态机风格
// 初始化状态为 green 绿色
lightService.send('TIMER') // yellow
lightService.send('TIMER') // red

// 批量活动
lightService.send([
  'TIMER',
  'TIMER'
])

// 中止
lightService.stop()

// 从特定状态启动当前服务,这对于状态的保存以及使用更有做用
lightService.start(previousState)

咱们也能够结合其余库在 Vue React 框架中使用,仅仅只用几行代码就实现了咱们想要的功能。

import lightMachine from '..'
// react hook 风格
import { useMachine } from '@xstate/react'

function Light() {
  const [light, send] = useMachine(lightMachine)
  
  return <>
    // 当前状态 state 是不是绿色
    <span>{light.matches('green') && '绿色'}</span>    
    // 当前状态的值
    <span>{light.value}</span>  
    // 发送消息
    <button onClick={() => send('TIMER')}>切换</button>
  </>
}

当前的状态机也是还能够进行嵌套处理,在红灯状态下添加人的行动状态。

import { Machine } from 'xstate';

const pedestrianStates = {
  // 初识状态 行走
  initial: 'walk',
  states: {
    walk: {
      on: {
        PED_TIMER: 'wait'
      }
    },
    wait: {
      on: {
        PED_TIMER: 'stop'
      }
    },
    stop: {}
  }
};

const lightMachine = Machine({
  id: 'light',
  initial: 'green',
  states: {
    green: {
      on: {
        TIMER: 'yellow'
      }
    },
    yellow: {
      on: {
        TIMER: 'red'
      }
    },
    red: {
      on: {
        TIMER: 'green'
      },
      ...pedestrianStates
    }
  }
});

const currentState = 'yellow';

const nextState = lightMachine.transition(currentState, 'TIMER').value;

// 返回级联对象 
// => {
//   red: 'walk'
// }

// 也能够写为 red.walk
lightMachine.transition('red.walk', 'PED_TIMER').value;

// 转化后返回
// => {
//   red: 'wait'
// }

// TIMER 还能够返回下一个状态
lightMachine.transition({ red: 'stop' }, 'TIMER').value;
// => 'green'

固然了,既然有嵌套状态,咱们还能够利用 type: 'parallel' ,进行串行和并行处理。

除此以外,XState 还有扩展状态 context 和过分防御 guards。这样的话,更可以模拟现实生活

// 是否能够编辑
functions canEdit(context: any, event: any, { cond }: any) {
  console.log(cond)
  // => delay: 1000
  
  // 是否有某种权限 ???
  return hasXXXAuthority(context.user)
}


const buttonMachine = Machine({
  id: 'buttons',
  initial: 'green',
  // 扩展状态,例如 用户等其余全局数据
  context: {
    // 用户数据
    user: {}
  },
  states: {
    view: {
      on: {
        // 对应以前 TIMRE: 'yellow'
        // 实际上 字符串没法表达太多信息,须要对象表示
        EDIT: {
          target: 'edit',
          // 若是没有该权限,不进行转换,处于原状态
          // 若是没有附加条件,直接 cond: searchValid
          cond: {
            type: 'searchValid',
            delay: 3
          }
        }, 
      }
    }
  }
}, {
  // 守卫
  guards: {
    canEdit,
  }
})


// XState 给予了更加合适的 API 接口,开发时候 Context 可能不存在
// 或者咱们须要在不一样的上下文 context 中复用状态机,这样代码扩展性更强
const buttonMachineWithDelay = buttonMachine.withContext({
  user: {},
  delay: 1000
})

// withContext 是直接替换,不进行浅层合并,可是咱们能够手动合并
const buttonMachineWithDelay = buttonMachine.withContext({
  ...buttonMachine.context,
  delay: 1000
})

咱们还能够经过瞬时状态来过分,瞬态状态节点能够根据条件来肯定机器应从先前的状态真正进入哪一个状态。瞬态状态表现为空字符串,即 '',如

const timeOfDayMachine = Machine({
  id: 'timeOfDay',
  // 当前不知道是什么状态
  initial: 'unknown',
  context: {
    time: undefined
  },
  states: {
    // Transient state
    unknown: {
      on: {
        '': [
          { target: 'morning', cond: 'isBeforeNoon' },
          { target: 'afternoon', cond: 'isBeforeSix' },
          { target: 'evening' }
        ]
      }
    },
    morning: {},
    afternoon: {},
    evening: {}
  }
}, {
  guards: {
    isBeforeNoon: //... 确认当前时间是否小于 中午 
    isBeforeSix: // ... 确认当前时间是否小于 下午 6 点
  }
});

const timeOfDayService = interpret(timeOfDayMachine
  .withContext({ time: Date.now() }))
  .onTransition(state => console.log(state.value))
  .start();

timeOfDayService.state.value 
// 根据当前时间,能够是 morning afternoon 和 evening,而不是 unknown 转态

到这里,我以为已经介绍 XState 不少功能了,篇幅所限,不能彻底介绍全部功能,不过当前的功能已经足够大部分业务需求使用了。若是有其余更复杂的需求,能够参考 XState 文档

这里列举一些没有介绍到的功能点:

  • 进入和离开某状态触发动做(action 一次性)和活动(activity 持续性触发,直到离开某状态)
  • 延迟事件与过分 after
  • 服务调用 invoke,包括 promise 以及 两个状态机之间相互交互
  • 历史状态节点,能够经过配置保存状态而且回退状态

固然了,对比于 x-state 这种,还有其余的状态机工具,如 javascript-state-machine , Ego 等。你们能够酌情考虑使用。

总结

对于现代框架而言,不管是如火如荼的 React Hook 仍是渐入佳境的 Vue Compoistion Api,其本质都想提高状态逻辑的复用能力。可是考虑大部分场景下,状态自己的切换都是有特定约束的,若是仅仅靠良好的编程习惯,恐怕仍是难以写出抑郁修改的代码。而 FSM 以及 XState 无疑是一把利器。

鼓励一下

若是你以为这篇文章不错,但愿能够给与我一些鼓励,在个人 github 博客下帮忙 star 一下。

博客地址

参考

XState 文档

JavaScript与有限状态机

相关文章
相关标签/搜索