返回顶部
首页 > 资讯 > 前端开发 > JavaScript >React使用有限状态机的实现示例
  • 398
分享到

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

2024-04-02 19:04:59 398人浏览 薄情痞子
摘要

目录什么是有限状态机?有限状态机的示例有限状态机和软件开发、计算机科学有什么关系?举了那么多例子,但我该怎么展示在前端开发中使用状态机呢?React 应用程序中的注册表单的传统实现方

在 React 中使用有限状态机,似乎不是一个寻常的话题。因为有限状态机通常和前端关系不大。 但是最近我发现了一个非常棒的技巧,可以在复杂的 React 项目中发挥有限状态机的作用。可以很好的提高程序的安全性。 下面我们就来看看吧。

什么是有限状态机?

有限状态机,英文是 Finite State Machine,简称 FSM,有时候也被称为有限状态自动机(Finite State AutoMation)。它是一种描述系统行为的数学计算模型,也就是一种抽象机。它可以替代图灵机等其他类型的模型。 有限状态机由给定对象的所有可能状态以及它们之间的转换组成。和图灵机相比,它的计算能力较低。这种计算能力的区别意味着 FSM 的计算能力更加有限,因为它具有有限数量的状态,不如的话就是无限状态机了。 更重要的是:状态机的一条规则是:它在任何时候都只处于一种状态。 由于有限状态机会根据适当的组合或预定的时间顺序产生某些动作,因此我们可以在现代社会的任何地方找到有限状态机的影子。这些包括自动售货机、电梯,甚至是红绿灯。

有限状态机的示例

一个现实中完美匹配有限状态机的例子是红绿灯。我们来分析一下红绿灯的工作方式: 它有四种状态:

  • 停车-红灯。
  • 准备开车-红灯和黄灯。
  • 开车-绿灯。
  • 准备停车-黄灯。

它有四种状态的转换:

  • 停车->准备开车。
  • 准备开车->开车
  • 开车->准备停车。
  • 准备停车->停车。

我们可以看到,我们有有限数量的状态和状态的转换。另外,红绿灯在任何时候都只能处于一种状态。这意味着我们在这处理的是有限状态机。 更重要的是,通过实现有限状态机,我们可以保证,模型不会发生意外。以红绿灯为例,红绿灯绝对不会出现直接从绿灯转换成红灯的情况。

有限状态机和软件开发、计算机科学有什么关系?

其实有很多关系。特别是游戏开发,很多游戏中都会大量使用有限状态机。 举个例子,大家应该都玩过超级马里奥这款 2D 游戏。马里奥在游戏里可以做什么呢? 他可以: 静止、朝右走、朝左走、跳跃。 从代码的角度来看,它对应的就是摇杆事件。

  • 什么都不按-默认设置静止状态。
  • 按左键-触发设置朝左走状态的朝左走事件。
  • 按右键-触发设置朝右走状态的朝右走事件。
  • 按跳跃键-触发设置跳跃状态的跳跃事件。
  • 松开按键-触发设置静止状态的静止事件。

举了那么多例子,但我该怎么展示在前端开发中使用状态机呢?

无论是从上面的概念还是具体的场景,我都是想保证你对有限状态机有一个了解。 接下来我来讲讲有限状态机在前端的应用场景。 首先我得承认,在前端开发中有限状态机并不是那么常见。我认为这个现象的主要原因是因为它不是实现功能最简单也不是最快的方法。 这有点像 TypeScript,它会让你慢一点,它会带来一些复杂性。但最终每个人都会从中受益。 为了证明我的这种观点并非毫无根据,我会展示一个我曾经开发的 React 项目中使用有限状态机的示例。 这是一个很简单的注册表单,分为三个部分。每个部分都会根据当前填写的进度进行渲染。

React 应用程序中的注册表单的传统实现方式

我先快速演示一下我实现上述表单功能的方法。 首先,我要定义所有的组件以及初始状态。

const Step = {
  Company: 0,
  Director: 1,
  Contact: 2,
} as const;

const Views = [<CompanyDataFORMPart />, <DirectorDataFormPart />, <ContactDataFormPart />];

const initialStep = Step.Account

接下来我们定义状态:

const [currentStep, setCurrentStep] = useState<number>(initialStep)

最后是组件本身:

<>
  <div className="stepsContainer">
    <Steps current={currentStep} labelPlacement="vertical" size="small">
      {Object.keys(Step).map(s => (
        <Steps.Step title={s} />
      ))}
    </Steps>
  </div>

  <Spacer />

  <FormPart
    onPrevious={() => {
      setCurrentStep(prev => prev - 1);
    }}
    onNext={() => {
      setCurrentStep(prev => prev + 1);
    }}
      >
    {Views[currentStep]}
  </FormPart>
</>

这一切看上去似乎很正常,表单可以切换到下一个和上一个步骤。但这里存在一个很明显的错误。 那就是程序没有考虑边界问题。这意味着 currentStep 的值可能超过最大步骤,也就是 2,也可能低于 0。 如果要修复它,我们会写出下列代码:

onPrevious={() => {
  setCurrentStep(prev => Math.max(prev - 1, 0))
}}
onNext={()=>{
  setCurrentStep(prev => Math.min(prev + 1, Views.length - 1))
}}

还有其他风险

这个代码运行起来确实没有问题,但还是会有一些潜在风险。 在软件开发中,很少会出现你一个人负责整个项目的情况,一般来说是一整个团队在协作,这也就意味着有许多其他开发人员会检查你的代码,并且会视图理解它并可能会修改它。 我们假设有个人在表单顶部写了一个方法,直接跳到了第三步。

const functionWithBadLogic = () => {
  setCurrentStep(3);
}

这是一个很好的反面教材,第三步在我们的表单中压根就不存在。 另外一个例子是下面这样的:

onNext={() => {
  setCurrentStep(prev => Math.min(prev + 2, Views.length -1))
}}

在这个代码中有什么问题吗?如果给定顺序中需要所有的步骤,为什么会有人跳过一个步骤? 这是最后一个例子:

const Step = {
  Company: 0,
  Director: 1,
  Contact: 2,
} as const;

const Views = [
  <CompanyDataFormPart />,
  <DirectorDataFormPart />,
  <ContactDataFormPart />,
  <div>I should not be there!</div>
]

这些错误中的任何一个投入生产都可能会出现下面这种情况:

这似乎看上去不是什么大问题

也许确实不是什么大问题。但是你要知道,我的例子是很简单的一个流程。在真实的项目中,会有更大更复杂的恶项目,例如用于银行和汇款的金融类程序,用于审批和工作流的办公类程序。 如果我们在一个地方定义所有可能出现的状态和状态之间的转换,会不会更容易?类似于某种约定,我们可以很容易的查看其中的整个逻辑,并且确保不会发生其他任何约定之外的事情。 实现这种模式的那个东西就是有限状态机。

把表单重构为有限状态机

首先,我们先来只关注 onNext 和 onPrevious 函数。我们想要制造一台机器,我们用下面的状态和事件来描述它的行为,也就是为这台机器的特性设计一个模型。

状态

  • company
  • director
  • contact

事件

  • next:按顺序切换到下一个状态。
  • prev:按顺序切换到上一个状态。

实现起来像下面这样:

const formMachine = createMachine({
  id: 'formState',
  initial: 'company',
  states: {
    company: {
      on: {
        next: { target: 'director' }
      }
    },
    director: {
      on: {
        previous: { target: 'company' },
        next: { target: 'contact' },
      },
    },
    contact: {
      on: {
        previous: { target: 'director' },
      },
    },
  }
})

现在让我们来分析一下这段代码。 createMachine 方法接受由 id、initial 和 states 共同组成的对象,这三个字端的作用分别是:

  • id:唯一标识符。
  • initial:初始状态
  • states:所有状态,其中的键是状态的名称,值是描述状态的对象。

接下来我们再分析一下 director 这个状态:

  • 它有一个名字,叫做 director。
  • 它可以对两个事件做出反应:previous 事件,将状态转换为 company。next 事件,将状态设置为 contacct。

使用 xstate 将有限状态机可视化

感谢 xstate 的开发人员,我们可以将上面的代码粘贴到 xstate 的在线可视化编辑器中。这个工具可以展示出有限状态机的所有可能的状态和事件。 我们的状态机是这个样子:

我承认为了实现这样一个简单的小功能编写这么多代码似乎有些过度设计,但是我们继续往下看,我保证你会相信使用有限状态机是值得的。

通过 9 个步骤完成重构

我们实现了有限状态机,但是我们还没有做最重要的事情。我们必须重构渲染逻辑。 接下来我要为有限状态机实现一些上下文。

步骤1: 为上下文添加类型定义

type Context = {
  currentView: Reactnode;
}

步骤2: 添加将状态映射到组件的函数

const mapStateToComponent: Record<string, ReactNode> = {
  company: <CompanyDataFormPart />,
  director: <DirectorDataFormPart />,
  contact: <ContactDataFormPart />,
}

步骤3: 将上下文添加到有限状态机的定义中

context: {
  currentView: <CompanyDataFormPart />,
}

步骤4: 定义一个将改变上下文的函数

const changeComponent = assign<Context>({
  currentViewe: (context, event, { action }) => {
    return mapStateToComponent[action.payload as string];
  }
})

步骤5: 将这个函数添加到有限状态机的 actions 中

{
  actions: {
    changeComponent,
  }
}

步骤6: 将由 previous 和 next 事件触发这个操作

{
  director: {
    on: {
      previous: {
        target: 'company',
        actions: {
          type: 'changeComponent',
          payload: 'company'
        }
      },
      next: {
        target: 'contact',
          actions: {
            type: 'changeComponent',
            payload: 'contact'
          }
        }
    }
  }
}

步骤7: 向组件添加 useMachine Hook

const [current, send] = useMachine(formMachine)

步骤8: 通过 onPrevious 和 onNext 函数将事件发送到有限状态机

onPrevious={() => {
  send('previous');
}}
onNext={() => {
  send('next');
}}

步骤9: 渲染当前状态对应的组件

{current.context.currentView}

我们马上就要完成了!

有限状态机的安全性,使得我们的表单同样安全

我们再回来看看之前举的最后一个反例。

const Step = {
  Company: 0,
  Director: 1,
  Contact: 2,
} as const;

const Views = [
  <CompanyDataFormPart />,
  <DirectorDataFormPart />,
  <ContactDataFormPart />,
  <div>I should not be there!</div>
]

可以看到,Step 和 Views 是解耦的。我们通过逐步渲染分页进度面板的值来进行映射,并且使用当前索引来渲染 Views 数组中的元素。 在我们的有限状态机中如何用更好的方式来实现这一点? 我们首先来稍微改变一下上下文。

export type View = {
  Component: ReactNode;
  step: number;
}

export type Context = {
  currentView: View;
}

接下来修改一下 mapStateToComponent 这个函数,顺便把函数名也改掉。

const mapStateToView: Record<string, View> = {
  company: {
    Component: <CompanyDataFormPart />,
    step: 0,
  },
  director: {
    Component: <DirectorDataFormPart />,
    step: 1,
  },
  contact: {
    Component: <ContactDataFormPart />,
    step: 2,
  },
};

最后为我们的有限状态机添加一些类型,将类型和 actions 移到不同的文件里。 现在我们的代码像下面这样: formMachine.types.ts

import { ReactNode } from 'react';
import { StateNode } from 'xstate';

export type Event = { type: 'NEXT' } | { type: 'PREVIOUS' };

export type View = {
  Component: ReactNode;
  step: number;
};

export type Context = {
  currentView: View;
};

export type State = {
  states: {
    company: StateNode;
    director: StateNode;
    contact: StateNode;
  };
};

formMachine.actions.ts

import { assign } from 'xstate';

import { CompanyDataFormPart } from '../components/CompanyDataFormPart/CompanyDataFormPart';
import { ContactDataFormPart } from '../components/ContactDataFormPart/ContactDataFormPart';
import { DirectorDataFormPart } from '../components/DirectorDataFormPartt/ContactDataFormPart';

import { Context, View } from './formMachine.types';

export const mapNameToView: Record<string, View> = {
  company: {
    Component: <CompanyDataFormPart />,
    step: 0,
  },
  director: {
    Component: <DirectorDataFormPart />,
    step: 1,
  },
  contact: {
    Component: <ContactDataFormPart />,
    step: 2,
  },
};

export const changeView = assign<Context, Event>({
  currentView: (_context, _event, { action }) => {
    if (typeof action.payload !== 'string') {
      throw new Error('Action payload should be string');
    }

    return mapNameToView[action.payload];
  },
});

formMachine.ts

import { MachineConfig, MachineOptions, createMachine } from 'xstate';

import { mapNameToView, changeView } from './formMachine.actions';
import { State, Context } from './formMachine.types';

const initialStateName = 'company';

const formMachineConfig: MachineConfig<Context, State, Event> = {
  id: 'formState',
  initial: initialStateName,
  context: {
    currentView: mapNameToView[initialStateName],
  },
  states: {
    company: {
      on: {
        NEXT: { target: 'director', actions: { type: 'changeView', payload: 'director' } },
      },
    },
    director: {
      on: {
        PREVIOUS: { target: 'company', actions: { type: 'changeView', payload: 'company' } },
        NEXT: { target: 'contact', actions: { type: 'changeView', payload: 'contact' } },
      },
    },
    contact: {
      on: {
        PREVIOUS: { target: 'director', actions: { type: 'changeView', payload: 'director' } },
      },
    },
  },
};

const formMachineOptions: Partial<MachineOptions<Context, Event>> = {
  actions: { changeView },
};

export const formMachine = createMachine(formMachineConfig, formMachineOptions);

export const formMachineStates = Object.keys(formMachine.states);

App.tsx

import React from 'react';
import { useMachine } from '@xstate/react';
import Steps from 'rc-steps';

import { FormPart } from './components/FormPart/FormPart';
import { Spacer } from './components/Spacer/Spacer';
import { formMachine, formMachineStates } from './formMachine/formMachine';

function App() {
  const [current, send] = useMachine(formMachine);

  return (
    <div className="app">
      <div className="stepsContainer">
        <Steps current={current.context.currentView.step} labelPlacement="vertical" size="small">
          {formMachineStates.map(s => (
            <Steps.Step title={s} key={s} />
          ))}
        </Steps>
      </div>

      <Spacer />

      <FormPart
        onPrevious={() => {
          send('PREVIOUS');
        }}
        onNext={() => {
          send('NEXT');
        }}
      >
        {current.context.currentView.Component}
      </FormPart>
    </div>
  );
}

在 React 中使用有限状态机的概括

可能你会说,“它看起来仍然很复杂。”。但是请你记住,如果你正在为一家大公司研发一个非常重要的项目,那其中一点微小的错误都可能会导致非常严重的资金损失。 最后我总结一下使用有限状态机的几个优势:

  • 类型安全。我们永远都不会使用在类型定义中的状态以外的状态,否则会编译错误。
  • 不会有错误的状态和错误的转换。如果不改变有限状态机的定义,那么不可能有人能够做到从第1步直接跳转到第3步。
  • 所有的逻辑都在一个位置进行描述。

到此这篇关于React使用有限状态机的实现示例的文章就介绍到这了,更多相关React 有限状态机内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

--结束END--

本文标题: React使用有限状态机的实现示例

本文链接: https://lsjlt.com/news/149427.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • React使用有限状态机的实现示例
    目录什么是有限状态机?有限状态机的示例有限状态机和软件开发、计算机科学有什么关系?举了那么多例子,但我该怎么展示在前端开发中使用状态机呢?React 应用程序中的注册表单的传统实现方...
    99+
    2024-04-02
  • C++有限状态机实现详解
    目录有限状态机四大要素C++函数指针实现总结前提:因为最近打算学设计模式的状态模式,但不是很明白有限状态机和状态模式之间的关系,索性用C++实现了一个简单案例复习了一下FSM,如果有...
    99+
    2024-04-02
  • JS前端实现fsm有限状态机实例详解
    目录引言举个栗子从零开始获取状态状态改变transition实现fsm状态机实现钩子函数完整代码总结引言 我们平时开发时本质上就时对应用程序的各种状态进行切换并作出相应处理,最直接的...
    99+
    2024-04-02
  • react实现组件状态缓存的示例代码
    目录前言一、安装第三方库二、配置操作总结前言 在移动端中,用户访问了一个列表页,上拉浏览列表页的过程中,随着滚动高度逐渐增加,数据也将采用触底分页加载的形式逐步增加,列表页浏览到某个...
    99+
    2023-02-24
    react 组件状态缓存 react 组件缓存
  • C++怎么实现一个有限状态机
    本篇内容介绍了“C++怎么实现一个有限状态机”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!什么是有限状态机?简单说就是作一件事可能会经过多个...
    99+
    2023-06-03
  • Java实现有限状态机的推荐方案分享
    目录一、背景 二、推荐方式2.1 自定义的枚举 2.2 外部枚举 三、总结 一、背景 平时工作开发过程中,难免会用到状态机(状态的流转)。 如奖学金审批流程、请假审批流程、竞标流程...
    99+
    2024-04-02
  • React状态的不变性实例详解
    目录正文什么是 immutableReact 与 Immutability修改 state 的错误案例修改 state 的正确案例总结正文 不变性对应的英文单词是 Immutabil...
    99+
    2022-11-16
    React 状态不变性 React 状态
  • React-Router(V6)的权限控制实现示例
    目录接口权限页面权限1. 菜单权限2. 路由权限按钮权限参考在一个后台管理系统中,安全是很重要的。不光后端需要做权限校验,前端也需要做权限控制。 我们可以大致将权限分为3种: 接口权...
    99+
    2023-05-19
    React-Router权限控制 React-Router权限
  • React使用高德地图的实现示例(react-amap)
    pc版React重构,使用到了高德地图。搜了资料,发现有一个针对React进行封装的地图插件react-amap。官方网址:https://elemefe.github.io/rea...
    99+
    2024-04-02
  • Android Studio 透明状态栏的实现示例
        最近正在学习使用Android Studio,发现默认的Hello World程序界面和我们平时使用的APP界面有很大的不同,多了Ac...
    99+
    2024-04-02
  • Mobx实现React 应用的状态管理详解
    目录MobX从一个 demo 开始创建类并将其转化成可观察对象使用可观察对象MobX 与 React 集成在组件中使用可观察对象1. 访问全局的类实例2. 通过 props3. 通过...
    99+
    2022-12-08
    Mobx React 应用状态管理 Mobx React
  • Android沉浸式状态栏实现示例
    应用市场上App越来越多的出现沉浸式状态栏的设计(如下图所示)状态栏和导航栏具有相同的颜色。Android在4.4开始对于该种效果的支持,而在4.4之下,状态栏只是黑框,无法控...
    99+
    2022-06-06
    示例 状态栏 Android
  • react18中react-redux状态管理的实现
    react的状态管理还是挺多的现在流行的有以下五种: RecoilMobXXStateReduxContext 今天我们来讲一下react众多状态管理之一的redux,虽然这个我不太...
    99+
    2024-04-02
  • Python编程使用有限状态机识别地址有效性
    在收发快递填写地址的时候,我们会经常手动输入地址让程序智能识别,标准的地址比如,xx省xx市xx县/区xx路xx号,不过有时候也可以简单写:xx市xx县/区xx路xx号,或者xx省x...
    99+
    2024-04-02
  • React less 实现纵横柱状图示例详解
    目录引言主要设计来源display 布局display 布局动态位置使用绝对定位styleJS引言 之前的文章,咱们介绍过横向和竖向,具体的内容,请看 React + CSS 绘制横...
    99+
    2024-04-02
  • 使用vue-router实现动态权限控制的示例分析
    这篇文章将为大家详细讲解有关使用vue-router实现动态权限控制的示例分析,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。使用vue开发带权限管理系统,尤其是采用了vu...
    99+
    2024-04-02
  • python状态机transitions库的示例分析
    这篇文章主要介绍了python状态机transitions库的示例分析,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。一、简介 transitions库pip&nbs...
    99+
    2023-06-15
  • Flutter状态管理Provider的使用示例详解
    目录前言计数器全局状态总结前言 Provider是三大主流状态管理框架官方推荐使用的框架,它是基于官方数据共享组件InheritedWidget实现的,通过数据改变调用生命周期中的d...
    99+
    2022-11-13
    Flutter状态管理Provider Flutter Provider
  • Android实现状态栏(statusbar)渐变效果的示例
    前言qq最近更新搞了渐变式状态栏.然后...新需求就是要加这个.唉先来张效果图:常见的方式:设置Theme,状态栏透明. <item name="android:windowTranslucentStatus">true<...
    99+
    2023-05-30
    android 状态栏 渐变
  • SAP用户状态的示例分析
    小编给大家分享一下SAP用户状态的示例分析,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!SAP用户状态详解SAP存放业务对象的表在sap中,包括订单、项目(项目立...
    99+
    2023-06-05
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作