关于前端:探索FSM-有限状态机应用

3次阅读

共计 6156 个字符,预计需要花费 16 分钟才能阅读完成。

咱们是袋鼠云数栈 UED 团队,致力于打造优良的一站式数据中台产品。咱们始终保持工匠精力,摸索前端路线,为社区积攒并流传教训价值。。

本文作者:木杪

无限状态机(FSM) 是计算机科学中的一种数学模型,可用于示意和控制系统的行为。它由一组状态以及定义在这些状态上的转换函数组成。FSM 被宽泛用于计算机程序中的状态机制。

无限状态机(FSM)利用场景

  • 在各种自动化零碎的利用: 例如交通信号灯、地铁站的旋转闸门、银行主动取款机等。通过对状态和转换函数的定义,能够实现对系统行为的准确管制。

    交通信号灯状态流转图

    地铁站的旋转闸门状态流转图

    银行主动取款机状态流转图

  • 在编程畛域的利用: 例如在编写编译器和解释器时,能够应用 无限状态机(FSM) 来解决词法剖析。例如:JSON.Parse
  • 在 Notion 中利用: 能够应用 无限状态机(FSM) 的相干概念来构建各种工作流程,例如状态转换图、状态转换表等。
  • 在 web 中利用: 咱们相熟的 Promise 也是一个状态机,具备三个状态:pending、resolved。rejected。

    Promise 状态流转图

    登录性能流转图

相似这样的状态机的例子不可胜数,甚至于,人也是一种极其简单的状态机,给定一种刺激或多种刺激组合,也会触发人从某种状态过渡到另一种状态。只不过复杂程度极高,以至于现代科学齐全无奈解密这种状态机。

无限状态机(FSM)实现原理

具体来说,FSM 由以下几局部组成:

  • 初始状态:零碎的初始状态。
  • 状态汇合:示意零碎可能处于的各种状态。
  • 转移函数:定义零碎在不同状态之间的转移条件和后果。
  • 终止状态:零碎在某个状态下能够进行计算。

无限状态机 (FSM) 的实现基于 状态转移图 状态转移图 是一个有向图,它示意 无限状态机 (FSM) 中状态之间的转移关系。在 状态转移图 中,每个状态示意零碎的某种状态,每个转移示意零碎从一个状态转移到另一个状态的条件和后果。

实现繁难的无限状态机(FSM)

实现步骤

  • 当状态机开始执行时,它会主动进入初始化状态(initial state)。
  • 每个状态都能够定义,在进入(onEnter)或退出(onExit)该状态时产生的行为事件(actions),通常这些行为事件会携带副作用(side effect)。
  • 每个状态都能够定义触发转换(transition)的事件。
  • 转换定义了在退出一个状态并进入另一个状态时,状态机该如何解决这种事件。
  • 在状态转换产生时,能够定义能够触发的行为事件,从而个别用来表白其副作用。

    状态转移图

function createMachine(stateMachineDefinition) {
  const machine = {
    value: stateMachineDefinition.initialState,
    performTransition(currentState, event) {const currentStateDefinition = stateMachineDefinition[currentState];
      const destinationTransition = currentStateDefinition.transitions[event];
      if (!destinationTransition) {return;}
      const destinationState = destinationTransition.target;
      const destinationStateDefinition =
        stateMachineDefinition[destinationState];

      destinationTransition.action();
      currentStateDefinition.actions.onExit();
      destinationStateDefinition.actions.onEnter();

      machine.value = destinationState;

      return machine.value;
    },
  };
  return machine;
}

const machine = createMachine({
  initialState: "off",
  off: {
    actions: {onEnter() {console.log("off: onEnter");
      },
      onExit() {console.log("off: onExit");
      },
    },
    transitions: {
      switch: {
        target: "on",
        action() {console.log('transition action for"switch"in"off"state');
        },
      },
    },
  },
  on: {
    actions: {onEnter() {console.log("on: onEnter");
      },
      onExit() {console.log("on: onExit");
      },
    },
    transitions: {
      switch: {
        target: "off",
        action() {console.log('transition action for"switch"in"on"state');
        },
      },
    },
  },
});

let state = machine.value;
console.log(`current state: ${state}`);
state = machine.performTransition(state, "switch");
console.log(`current state: ${state}`);
state = machine.performTransition(state, "switch");
console.log(`current state: ${state}`);

无限状态机(FSM)的 利用实现

在状态比拟多的状况下,把状态、事件及 transitions 集中到一个状态机中,进行对立治理。这样不须要写太多的 if-else,或者 case 判断,如果减少状态和事件,也便于代码的保护和扩大。

文本解析器

实现思路

  • 确定状态和输出
    在编写 FSM 之前,咱们须要确定咱们的状态和输出。在这个例子中,咱们将定义三个状态:起始状态、数字状态和字符串状态。咱们还将定义四个输出:数字、字母、引号和空格。
  • 定义状态机类
    当初,咱们能够编写代码来实现咱们的 FSM。咱们须要定义一个状态机类,它将承受输出,并依据转移规定转换状态。该类应该蕴含以下属性:

    • currentState:以后状态。
    • states:状态列表。
    • transitions:转移列表。
      它还应该蕴含以下办法:
    • transition:该办法承受一个输出参数 input,依据以后状态以及输出参数,执行相应的状态转换。
  • 定义转移规定
    咱们还须要定义状态之间的转移规定。为此,咱们将应用转移列表,其中蕴含状态之间的映射和输出。转移规定应该思考以后状态和输出,并依据它们确定下一个状态。如果以后状态和输出没有匹配的转移规定,则应该抛出一个异样。
  • 解析文本
    当初,咱们能够应用状态机解析文本。咱们须要将文本拆分为单词,并将每个单词作为输出提供给状态机。在解决完所有输出后,咱们能够通过调用 getInputType 办法来获取解析的令牌。

    示例代码

const STATES = {
  START: "start",
  NUMBER: "number",
  STRING: "string",
};

const INPUTS = {
  NUMBER: "number",
  LETTER: "letter",
  SPACE: "space",
  QUOTE: "quote",
};

const TRANSITIONS = [
  {
    currentState: STATES.START,
    input: INPUTS.NUMBER,
    nextState: STATES.NUMBER,
  },
  {
    currentState: STATES.START,
    input: INPUTS.LETTER,
    nextState: STATES.STRING,
  },
  {currentState: STATES.START, input: INPUTS.SPACE, nextState: STATES.START},
  {currentState: STATES.START, input: INPUTS.QUOTE, nextState: STATES.STRING},
  {
    currentState: STATES.NUMBER,
    input: INPUTS.NUMBER,
    nextState: STATES.NUMBER,
  },
  {currentState: STATES.NUMBER, input: INPUTS.SPACE, nextState: STATES.START},
  {
    currentState: STATES.STRING,
    input: INPUTS.LETTER,
    nextState: STATES.STRING,
  },
  {currentState: STATES.STRING, input: INPUTS.SPACE, nextState: STATES.START},
  {currentState: STATES.STRING, input: INPUTS.QUOTE, nextState: STATES.START},
];

class TextParse {constructor() {
    this.currentState = STATES.START;
    this.buffer = "";
    this.type;
  }

  performTransition(input) {
    const transition = TRANSITIONS.find((t) => t.currentState === this.currentState && t.input === input.type
    );
    if (!transition)
      throw new Error(`Invalid input "${input.value}" for state "${this.currentState}"`
      );

    this.currentState = transition.nextState;

    if (this.currentState === STATES.START) {
      const token = this.buffer;
      const type = this.type;
      this.buffer = "";
      this.type = "";
      return {
        type,
        value: token,
      };
    } else {
      this.buffer += input.value;
      this.type = input.type;
    }
  }
}

function textParse(input) {const textParse = new TextParse();
  const tokens = [];

  for (let i = 0; i < input.length; i++) {const char = input[i];

    try {
      const token = textParse.performTransition({type: getInputType(char),
        value: char,
      });

      if (token) {tokens.push(token);
      }
    } catch (e) {console.error(e.message);
      return null;
    }
  }

    const lastToken = textParse.performTransition({type: INPUTS.SPACE});

  if (lastToken) {tokens.push(lastToken);
  }

  return tokens;
}

function getInputType(char) {if (/[0-9]/.test(char)) {return INPUTS.NUMBER;} else if (/[a-zA-Z]/.test(char)) {return INPUTS.LETTER;} else if (/[\s\n\t\r]/.test(char)) {return INPUTS.SPACE;} else if (char === '"') {return INPUTS.QUOTE;} else {throw new Error(`Unknown input type for "${char}"`);
  }
}

// Example usage:
console.log(textParse('123 abc"def ghi"456')); 
// [//   { type: 'number', value: '123'},
//   {type: 'letter', value: 'abc'},
//   {type: 'letter', value: '"def'},
//   {type: 'letter', value: 'ghi'},
//   {type: '', value:''},
//   {type: 'number', value: '456'}
// ]

示例代码

web 利用

应用 无限状态机(FSM) 联合 React 构建 web 利用,不局限于身份认证,登录,步骤表单,有蛮多 web 利用在
无限状态机(FSM)的实际 ,上面次要形容 从无限状态机(FSM)在服务端拉取数据的状态转移上的利用

  • 状态转移图
  • 状态集(States), 转换规则(Transitions)

    const states = {
    INITIAL: "idle",
    LOADING: "loading",
    SUCCESS: "success",
    FAILURE: "failure",
    };
    const transitions = {[states.INITIAL]: {fetch: () => /* Returns states.LOADING */,
    },
    
    [states.LOADING]: {},
    
    [states.SUCCESS]: {reload: () => /* Returns states.LOADING */,
      clear: () => /* Returns states.INITIAL */,},
    
    [states.FAILURE]: {retry: () => /* Returns states.LOADING */,
      clear: () => /* Returns states.INITIAL */,},
    }

    示例代码

    总结

    联合前端利用的摸索体现的不多,能够再作为第二篇内容去探讨,有趣味的同学能够尝试一下 无限状态机(FSM) 在 web 上的利用摸索,以及 Xstate 库(FSM 封装的功能性库) 的利用,以及跟 状态治理库 差异化的常识。在这里揭示一点,状态治理库 (Redux)Xstate 并不是互斥的,Xstate 关注的是如何设计状态,状态治理库关注的是如何治理状态。事实上,状态机简直能够与任何无主意的状态管理工具一起应用。我激励您摸索各种办法,以确定最适宜您、您的团队和您的应用程序的办法。

参考资料

  • https://statecharts.dev/what-is-a-state-machine.html
  • https://bespoyasov.me/blog/fsm-to-the-rescue/
  • https://xstate.js.org/docs/about/concepts.html
  • https://kentcdodds.com/blog/implementing-a-simple-state-machine-library-in-javascript
  • https://css-tricks.com/finite-state-machines-with-react/
正文完
 0