JS前端实现fsm有限状态机实例详解

目录
  • 引言
    • 举个栗子
  • 从零开始
    • 获取状态
    • 状态改变
    • transition
    • 实现fsm状态机
    • 实现钩子函数
  • 完整代码
  • 总结

引言

我们平时开发时本质上就时对应用程序的各种状态进行切换并作出相应处理,最直接的方法就是添加标志位然后考虑所有可能出现的边界问题,通过if...else if...else 来对当前状态进行判断从而达成页面的交互效果, 但随着业务需求的增加各种状态也会随之增多,我们就不得不再次修改if...else代码或者增加对应的判断,最终使得程序的可读性、扩展性、维护性变得很麻烦

有限状态机,(英语:Finite-state machine, FSM),又称有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。

利用有限状态机我们可以将条件判断的结果转化为状态对象内部的状态,并且能够使用对应的方法,进行对应的改变。这样方便了对状态的管理也会很容易,也是更好的实践了UI=fn(state)思想。

举个栗子

我们这里用一个简易的红绿灯案例,实现一个简易的有限状态机,并且可以通过每一个状态暴露出来的方法,改变当前的状态

const door = machine({
  RED: {
    yello: "YELLO",
  },
  GREEN: {
    red: "RED",
  },
  YELLO: {
    green: "GREEN",
  },
});
  • 首先初始时door的状态显示为红灯即RED
  • 当我们进行yello操作的时候,状态变成黄灯,即状态改变为YELLO
  • 当我们进行green操作的时候,状态变成绿灯,即状态改变为GREEN
  • 当我们连着进行red操作、yello操作的时候,最终状态变成黄灯,即状态改变为YELLO ...

从零开始

通过接受一个对象(如果是函数就执行),拿到初始值,并且在函数内部维护一个变量记录当前的状态,并且记录第一个状态为初始状态

const machine = statesObject => {
  if (typeof statesObject == "function") statesObject = statesObject();
  let currentState;
  for (const stateName in statesObject) {
    currentState = currentState || statesObject[stateName];
  }
};

获取状态

因为当前状态是通过函数局部变量currentState进行保存,我们需要一些方法

  • getMachineState:获取当前的状态
  • getMachineEvents:获取当前状态上保存了哪些方法

这两个函数通过stateMachine进行保存并作为函数结果进行返回

const machine = statesObject => {
  let currentState
  ...
  const getMachineState = () => currentState.name;
  const getMachineEvents = () => {
    const events = [];
    for (const property in currentState) {
      if (typeof currentState[property] == "function") events.push(property);
    }
    return events;
  };
  const stateMachine = { getMachineState, getMachineEvents };
  ...
  return stateMachine
};

状态改变

我们进行改变的时候,调用的是一开始配置好的方法对状态进行更改,此时需要将每一个状态合并到stateStore中进行保存

再将对应的方法作为偏函数(函数预先将转换的状态和方法进行传递),保存在stateMachinestateMachine会作为结果进行返回),这样就可以

  • 使用.yello().red().green()的方法,改变状态
  • 使用.getMachineState().getMachineEvents()查看当前状态和查看当前状态对应的方法
const machine = statesObject => {
  if (typeof statesObject == "function") statesObject = statesObject();
  let currentState;
  const stateStore = {};
  const getMachineState = () => currentState.name;
  const getMachineEvents = () => {
    const events = [];
    for (const property in currentState) {
      if (typeof currentState[property] == "function") events.push(property);
    }
    return events;
  };
  const stateMachine = { getMachineState, getMachineEvents };
  for (const stateName in statesObject) {
    stateStore[stateName] = statesObject[stateName];
    for (const event in stateStore[stateName]) {
      stateMachine[event] = transition.bind(null, stateName, event);
    }
    stateStore[stateName].name = stateName;
    currentState = currentState || stateStore[stateName];
  }
  return stateMachine;
};

transition

上面代码中最重要的莫过于transition函数,即改变当前状态,在stateStore中获取当前的要更改的状态名,重新给currentState赋值,并返回stateMachine供函数继续链式调用

const machine = statesObject => {
  ...
  const transition = (stateName, eventName) => {
    currentState = stateStore[stateName][eventName];
    return stateMachine;
  };
  for (const stateName in statesObject) {
    stateStore[stateName] = statesObject[stateName];
    for (const event in stateStore[stateName]) {
      stateMachine[event] = transition.bind(null, stateName, event);
    }
    stateStore[stateName].name = stateName;
    currentState = currentState || stateStore[stateName];
  }
  return stateMachine;
};

看似没有问题,但是如果我们按照上面的代码执行后,获得的状态值为undefined,因为我们在getMachineState时,获取到的是currentState.name,而不是currentState,所以此时在获取状态的时候需要用通过函数进行获取obj => obj[xxx]

const machine = statesObject => {
  ...
  const transition = (stateName, eventName) => {
    currentState = stateStore[stateName][eventName](stateStore);
    return stateMachine;
  };
  for (const stateName in statesObject) {
    stateStore[stateName] = statesObject[stateName];
    for (const event in stateStore[stateName]) {
      const item = stateStore[stateName][event];
      if (typeof item == "string") {
        stateStore[stateName][event] = obj => obj[item];
        stateMachine[event] = transition.bind(null, stateName, event);
      }
    }
    stateStore[stateName].name = stateName;
    currentState = currentState || stateStore[stateName];
  }
  return stateMachine;
};

实现fsm状态机

现在我们实现了一个完整的fsm,当我们配置好状态机时

const door = machine({
  RED: {
    yello: "YELLO",
  },
  GREEN: {
    red: "RED",
  },
  YELLO: {
    green: "GREEN",
  },
});

执行如下操作时,会打印我们想要的结果

  • door.getMachineState() --> RED
  • door.yello().getMachineState() --> YELLO
  • door.green().getMachineState() --> GREEN
  • door.red().yello().getMachineState() --> YELLO

实现钩子函数

但是我们监听不到状态机的改变,所以当我们想监听状态变换时,应该从内部暴露出钩子函数,这样可以监听到状态机内部的变化,又能进行一些副作用操作

对此,可以对transition进行一些改造,将对于currentState状态的改变用方法setMachineState去处理

setMachineState函数会拦截当前状态上绑定onChange方法进行触发,并将改变状态的函数改变前的状态改变后的状态传递出去

const machine = statesObject => {
  ...
  const setMachineState = (nextState, eventName) => {
    let onChangeState;
    let lastState = currentState;
    const resolveSpecialEventFn = (stateName, fnName) => {
      for (let property in stateStore[stateName]) {
        if (property.toLowerCase() === fnName.toLowerCase()) {
          return stateStore[stateName][property];
        }
      }
    };
    currentState = nextState;
    onChangeState = resolveSpecialEventFn(lastState.name, "onChange");
    if (
      onChangeState &&
      typeof onChangeState == "function" &&
      lastState.name != currentState.name
    ) {
      onChangeState.call(
        stateStore,
        eventName,
        lastState.name,
        currentState.name
      );
    }
  };
  const transition = (stateName, eventName) => {
    const curState = stateStore[stateName][eventName](stateStore);
    setMachineState(curState, eventName);
    return stateMachine;
  };
  ...
  return stateMachine;
};

这样我们在调用时,状态的每一次改变都可以监听到,并且可以执行对应的副作用函数

const door = machine({
  RED: {
    yello: "YELLO",
    onChange(fn, from, to) {
      console.log(fn, from, to, "onChange");
    },
  },
  GREEN: {
    red: "RED",
    onChange(fn, from, to) {
      console.log(fn, from, to, "onChange");
    },
  },
  YELLO: {
    green: "GREEN",
    onChange(fn, from, to) {
      console.log(fn, from, to, "onChange");
    },
  },
});

完整代码

export default statesObject => {
  if (typeof statesObject == "function") statesObject = statesObject();
  let currentState;
  const stateStore = {};
  const getMachineState = () => currentState.name;
  const getMachineEvents = () => {
    let events = [];
    for (const property in currentState) {
      if (typeof currentState[property] == "function") events.push(property);
    }
    return events;
  };
  const stateMachine = { getMachineState, getMachineEvents };
  const setMachineState = (nextState, eventName) => {
    let onChangeState;
    let lastState = currentState;
    const resolveSpecialEventFn = (stateName, fnName) => {
      for (let property in stateStore[stateName]) {
        if (property.toLowerCase() === fnName.toLowerCase()) {
          return stateStore[stateName][property];
        }
      }
    };
    currentState = nextState;
    onChangeState = resolveSpecialEventFn(lastState.name, "onChange");
    if (
      onChangeState &&
      typeof onChangeState == "function" &&
      lastState.name != currentState.name
    ) {
      onChangeState.call(
        stateStore,
        eventName,
        lastState.name,
        currentState.name
      );
    }
  };
  const transition = (stateName, eventName) => {
    const curState = stateStore[stateName][eventName](stateStore);
    setMachineState(curState, eventName);
    return stateMachine;
  };
  for (const stateName in statesObject) {
    stateStore[stateName] = statesObject[stateName];
    for (const event in stateStore[stateName]) {
      const item = stateStore[stateName][event];
      if (typeof item == "string") {
        stateStore[stateName][event] = obj => obj[item];
        stateMachine[event] = transition.bind(null, stateName, event);
      }
    }
    stateStore[stateName].name = stateName;
    currentState = currentState || stateStore[stateName];
  }
  return stateMachine;
};

总结

这个 fsm有限状态机 主要完成了:

  • 状态的可观测
  • 状态的链式调用
  • 状态变化的钩子函数

项目代码:github.com/blazer233/a…

参考轮子:github.com/fschaefer/S…

以上就是JS前端实现fsm有限状态机实例详解的详细内容,更多关于JS前端fsm有限状态机的资料请关注我们其它相关文章!

(0)

相关推荐

  • Linux有限状态机FSM的理解与实现

    有限状态机(finite state machine)简称FSM,表示有限个状态及在这些状态之间的转移和动作等行为的数学模型,在计算机领域有着广泛的应用.FSM是一种逻辑单元内部的一种高效编程方法,在服务器编程中,服务器可以根据不同状态或者消息类型进行相应的处理逻辑,使得程序逻辑清晰易懂. 那有限状态机通常在什么地方被用到? 处理程序语言或者自然语言的 tokenizer,自底向上解析语法的parser, 各种通信协议发送方和接受方传递数据对消息处理,游戏AI等都有应用场景. 状态机有以下几种实

  • Unity学习之FSM有限状态机

    前言:一个游戏里的一个人物会存在多种状态,那么就需要有一个专门管理这些状态的类.不然会显得杂乱无章,不易于后面状态的增加或者减少. 思路:既然要方便管理,那么首先肯定得有个系统类(专门用来存放所有的状态.状态的增删等功能):然后就是需要把所有的状态都单独写一个类(已达到修改某个状态的时候,其他状态不会受到影响). 状态管理类: using System.Collections; using System.Collections.Generic; using UnityEngine; public

  • Java实现有限状态机的推荐方案分享

    目录 一.背景 二.推荐方式 2.1 自定义的枚举 2.2 外部枚举 三.总结 一.背景 平时工作开发过程中,难免会用到状态机(状态的流转). 如奖学金审批流程.请假审批流程.竞标流程等,都需要根据不同行为转到不同的状态. 下面是一个简单的模拟状态机: 有些同学会选择将状态定义为常量,使用 if else 来流转状态,不太优雅. 有些同学会考虑将状态定义为枚举. 但是定义为枚举之后,大多数同学会选择使用 switch 来流转状态: import lombok.Getter; public enu

  • React使用有限状态机的实现示例

    目录 什么是有限状态机? 有限状态机的示例 有限状态机和软件开发.计算机科学有什么关系? 举了那么多例子,但我该怎么展示在前端开发中使用状态机呢? React 应用程序中的注册表单的传统实现方式 还有其他风险 把表单重构为有限状态机 状态 事件 使用 xstate 将有限状态机可视化 通过 9 个步骤完成重构 步骤1: 为上下文添加类型定义 步骤2: 添加将状态映射到组件的函数 步骤3: 将上下文添加到有限状态机的定义中 步骤4: 定义一个将改变上下文的函数 步骤5: 将这个函数添加到有限状态机

  • javascript与有限状态机详解

    简单说,它有三个特征: 复制代码 代码如下: * 状态总数(state)是有限的.* 任一时刻,只处在一种状态之中.* 某种条件下,会从一种状态转变(transition)到另一种状态. 它对JavaScript的意义在于,很多对象可以写成有限状态机. 举例来说,网页上有一个菜单元素.鼠标悬停的时候,菜单显示:鼠标移开的时候,菜单隐藏.如果使用有限状态机描述,就是这个菜单只有两种状态(显示和隐藏),鼠标会引发状态转变. 代码可以写成下面这样: 复制代码 代码如下: var menu = { //

  • JS前端实现fsm有限状态机实例详解

    目录 引言 举个栗子 从零开始 获取状态 状态改变 transition 实现fsm状态机 实现钩子函数 完整代码 总结 引言 我们平时开发时本质上就时对应用程序的各种状态进行切换并作出相应处理,最直接的方法就是添加标志位然后考虑所有可能出现的边界问题,通过if...else if...else 来对当前状态进行判断从而达成页面的交互效果, 但随着业务需求的增加各种状态也会随之增多,我们就不得不再次修改if...else代码或者增加对应的判断,最终使得程序的可读性.扩展性.维护性变得很麻烦 有限

  • JS JSOP跨域请求实例详解

    在项目开发中遇到跨域的问题,一般都是通过JSONP来解决的.但是JSONP到底是个什么东西呢,实现的原理又是什么呢.在项目的空闲时间可以好好的来研究一下了. 1.什么是JSONP? 要了解JSONP,不得不提一下JSON,那么什么是JSON? JSON is a subset of the object literal notation of JavaScript. Since JSON is a subset of JavaScript, it can be used in the langu

  • JS前端性能指标定位FMP使用详解

    目录 什么是FMP? 权重定位 权重计算 节点标记 计算权重值 第一步:简单粗暴,按大小计算 第二步:根据权重值推导主角元素 第三步:根据元素类型取时间 回归验证 什么是FMP? 可能大家对「白屏时间」这个名词并不陌生,他是「刀耕火种」年代,我们收集的页面性能指标之一,随着前端工程的复杂化,白屏时间已经没有什么实质性的意义了,取而代之的就是 FMP. 先来介绍几个与之相关的名词. FP(First Paint):首次绘制,标记浏览器渲染任何在视觉上不同于导航前屏幕内容的时间点 FCP(First

  • Vue.js框架路由使用方法实例详解

    Vue.js框架路由使用方法实例详解 html代码: <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name='viewport' content='width=device-width,initial-

  • Vue.js进行查询操作的实例详解

    Vue.js进行查询操作的实例详解 实例代码: <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <script src="../lib/vue.min.js" type="text/javascript" ></script> <title>字符转换</title> </head>

  • 原生js中ajax访问的实例详解

    原生js中ajax访问的实例详解 form表单中 登录名: 失去光标即触发事件 function createXmlHttp() { var xmlHttp; try { // Firefox, Opera 8.0+, Safari xmlHttp = new XMLHttpRequest(); } catch (e) { try {// Internet Explorer xmlHttp = new ActiveXObject("Msxml2.XMLHTTP"); } catch (

  • jQuery.datatables.js插件用法及api实例详解

    1.DataTables的默认配置 $(document).ready(function() { $('#example').dataTable(); } ); 示例:http://www.guoxk.com/html/DataTables/Zero-configuration.html 2.DataTables的一些基础属性配置 "bPaginate": true, //翻页功能 "bLengthChange": true, //改变每页显示数据数量 "

  • 微信小程序引用公共js里的方法的实例详解

    微信小程序引用公共js里的方法的实例详解 一个小程序页面由四个文件组成,一个小程序页面的四个文件具有相同路径与文件名,由此我们可知一个小程序页面对应着一个跟页面同名的js文件.可是当有些公共方法,我们想抽离出来成为一个独立公共的js文件.我们该如何实现呢. 在根目录下有一个app.js文件.这个根目录的js 文件我们可以通过getApp()轻松调用. //app.js App({ globaData:'huangenai' }) //test.js var app = getApp(); Pag

  • Android中webview与JS交互、互调方法实例详解

    Android中webview与JS交互.互调方法实例详解 前言: 对于试水的功能,一般公司都会采用H5的方式来开发,可以用很少的资源与很短的项目工期来完成. 但许多情况下,H5页面会需要一些原生持有的一些如用户信息之类的数据,一些交互也需要调用原生的,如toast之类要保持同一个手机风格一致的交互行为.这个时候就需要能够让JS主动调用原生的方法来进行操作或者获取数据.或者是原生调用JS的方法在H5加载的时候传递一些参数. 对于原生调用JS的方法 我们需要实现一个WebViewClient,在这

  • JS三级联动代码格式实例详解

    这篇文章主要介绍了JS三级联动代码格式实例详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 实现js多级联动的代码格式 <head runat="server"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title></title> </h

随机推荐