日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學(xué)無先后,達(dá)者為師

網(wǎng)站首頁(yè) 編程語言 正文

React使用有限狀態(tài)機(jī)的實(shí)現(xiàn)示例_React

作者:代碼與野獸 ? 更新時(shí)間: 2022-07-20 編程語言

在 React 中使用有限狀態(tài)機(jī),似乎不是一個(gè)尋常的話題。因?yàn)橛邢逘顟B(tài)機(jī)通常和前端關(guān)系不大。 但是最近我發(fā)現(xiàn)了一個(gè)非常棒的技巧,可以在復(fù)雜的 React 項(xiàng)目中發(fā)揮有限狀態(tài)機(jī)的作用。可以很好的提高程序的安全性。 下面我們就來看看吧。

什么是有限狀態(tài)機(jī)?

有限狀態(tài)機(jī),英文是 Finite State Machine,簡(jiǎn)稱 FSM,有時(shí)候也被稱為有限狀態(tài)自動(dòng)機(jī)(Finite State AutoMation)。它是一種描述系統(tǒng)行為的數(shù)學(xué)計(jì)算模型,也就是一種抽象機(jī)。它可以替代圖靈機(jī)等其他類型的模型。 有限狀態(tài)機(jī)由給定對(duì)象的所有可能狀態(tài)以及它們之間的轉(zhuǎn)換組成。和圖靈機(jī)相比,它的計(jì)算能力較低。這種計(jì)算能力的區(qū)別意味著 FSM 的計(jì)算能力更加有限,因?yàn)樗哂杏邢迶?shù)量的狀態(tài),不如的話就是無限狀態(tài)機(jī)了。 更重要的是:狀態(tài)機(jī)的一條規(guī)則是:它在任何時(shí)候都只處于一種狀態(tài)。 由于有限狀態(tài)機(jī)會(huì)根據(jù)適當(dāng)?shù)慕M合或預(yù)定的時(shí)間順序產(chǎn)生某些動(dòng)作,因此我們可以在現(xiàn)代社會(huì)的任何地方找到有限狀態(tài)機(jī)的影子。這些包括自動(dòng)售貨機(jī)、電梯,甚至是紅綠燈。

有限狀態(tài)機(jī)的示例

一個(gè)現(xiàn)實(shí)中完美匹配有限狀態(tài)機(jī)的例子是紅綠燈。我們來分析一下紅綠燈的工作方式: 它有四種狀態(tài):

  • 停車-紅燈。
  • 準(zhǔn)備開車-紅燈和黃燈。
  • 開車-綠燈。
  • 準(zhǔn)備停車-黃燈。

它有四種狀態(tài)的轉(zhuǎn)換:

  • 停車->準(zhǔn)備開車。
  • 準(zhǔn)備開車->開車
  • 開車->準(zhǔn)備停車。
  • 準(zhǔn)備停車->停車。

我們可以看到,我們有有限數(shù)量的狀態(tài)和狀態(tài)的轉(zhuǎn)換。另外,紅綠燈在任何時(shí)候都只能處于一種狀態(tài)。這意味著我們?cè)谶@處理的是有限狀態(tài)機(jī)。 更重要的是,通過實(shí)現(xiàn)有限狀態(tài)機(jī),我們可以保證,模型不會(huì)發(fā)生意外。以紅綠燈為例,紅綠燈絕對(duì)不會(huì)出現(xiàn)直接從綠燈轉(zhuǎn)換成紅燈的情況。

有限狀態(tài)機(jī)和軟件開發(fā)、計(jì)算機(jī)科學(xué)有什么關(guān)系?

其實(shí)有很多關(guān)系。特別是游戲開發(fā),很多游戲中都會(huì)大量使用有限狀態(tài)機(jī)。 舉個(gè)例子,大家應(yīng)該都玩過超級(jí)馬里奧這款 2D 游戲。馬里奧在游戲里可以做什么呢? 他可以: 靜止、朝右走、朝左走、跳躍。 從代碼的角度來看,它對(duì)應(yīng)的就是搖桿事件。

  • 什么都不按-默認(rèn)設(shè)置靜止?fàn)顟B(tài)。
  • 按左鍵-觸發(fā)設(shè)置朝左走狀態(tài)的朝左走事件。
  • 按右鍵-觸發(fā)設(shè)置朝右走狀態(tài)的朝右走事件。
  • 按跳躍鍵-觸發(fā)設(shè)置跳躍狀態(tài)的跳躍事件。
  • 松開按鍵-觸發(fā)設(shè)置靜止?fàn)顟B(tài)的靜止事件。

舉了那么多例子,但我該怎么展示在前端開發(fā)中使用狀態(tài)機(jī)呢?

無論是從上面的概念還是具體的場(chǎng)景,我都是想保證你對(duì)有限狀態(tài)機(jī)有一個(gè)了解。 接下來我來講講有限狀態(tài)機(jī)在前端的應(yīng)用場(chǎng)景。 首先我得承認(rèn),在前端開發(fā)中有限狀態(tài)機(jī)并不是那么常見。我認(rèn)為這個(gè)現(xiàn)象的主要原因是因?yàn)樗皇菍?shí)現(xiàn)功能最簡(jiǎn)單也不是最快的方法。 這有點(diǎn)像 TypeScript,它會(huì)讓你慢一點(diǎn),它會(huì)帶來一些復(fù)雜性。但最終每個(gè)人都會(huì)從中受益。 為了證明我的這種觀點(diǎn)并非毫無根據(jù),我會(huì)展示一個(gè)我曾經(jīng)開發(fā)的 React 項(xiàng)目中使用有限狀態(tài)機(jī)的示例。 這是一個(gè)很簡(jiǎn)單的注冊(cè)表單,分為三個(gè)部分。每個(gè)部分都會(huì)根據(jù)當(dāng)前填寫的進(jìn)度進(jìn)行渲染。

React 應(yīng)用程序中的注冊(cè)表單的傳統(tǒng)實(shí)現(xiàn)方式

我先快速演示一下我實(shí)現(xiàn)上述表單功能的方法。 首先,我要定義所有的組件以及初始狀態(tài)。

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

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

const initialStep = Step.Account

接下來我們定義狀態(tài):

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>
</>

這一切看上去似乎很正常,表單可以切換到下一個(gè)和上一個(gè)步驟。但這里存在一個(gè)很明顯的錯(cuò)誤。 那就是程序沒有考慮邊界問題。這意味著 currentStep 的值可能超過最大步驟,也就是 2,也可能低于 0。 如果要修復(fù)它,我們會(huì)寫出下列代碼:

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

還有其他風(fēng)險(xiǎn)

這個(gè)代碼運(yùn)行起來確實(shí)沒有問題,但還是會(huì)有一些潛在風(fēng)險(xiǎn)。 在軟件開發(fā)中,很少會(huì)出現(xiàn)你一個(gè)人負(fù)責(zé)整個(gè)項(xiàng)目的情況,一般來說是一整個(gè)團(tuán)隊(duì)在協(xié)作,這也就意味著有許多其他開發(fā)人員會(huì)檢查你的代碼,并且會(huì)視圖理解它并可能會(huì)修改它。 我們假設(shè)有個(gè)人在表單頂部寫了一個(gè)方法,直接跳到了第三步。

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

這是一個(gè)很好的反面教材,第三步在我們的表單中壓根就不存在。 另外一個(gè)例子是下面這樣的:

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

在這個(gè)代碼中有什么問題嗎?如果給定順序中需要所有的步驟,為什么會(huì)有人跳過一個(gè)步驟? 這是最后一個(gè)例子:

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

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

這些錯(cuò)誤中的任何一個(gè)投入生產(chǎn)都可能會(huì)出現(xiàn)下面這種情況:

這似乎看上去不是什么大問題

也許確實(shí)不是什么大問題。但是你要知道,我的例子是很簡(jiǎn)單的一個(gè)流程。在真實(shí)的項(xiàng)目中,會(huì)有更大更復(fù)雜的惡項(xiàng)目,例如用于銀行和匯款的金融類程序,用于審批和工作流的辦公類程序。 如果我們?cè)谝粋€(gè)地方定義所有可能出現(xiàn)的狀態(tài)和狀態(tài)之間的轉(zhuǎn)換,會(huì)不會(huì)更容易?類似于某種約定,我們可以很容易的查看其中的整個(gè)邏輯,并且確保不會(huì)發(fā)生其他任何約定之外的事情。 實(shí)現(xiàn)這種模式的那個(gè)東西就是有限狀態(tài)機(jī)。

把表單重構(gòu)為有限狀態(tài)機(jī)

首先,我們先來只關(guān)注 onNext 和 onPrevious 函數(shù)。我們想要制造一臺(tái)機(jī)器,我們用下面的狀態(tài)和事件來描述它的行為,也就是為這臺(tái)機(jī)器的特性設(shè)計(jì)一個(gè)模型。

狀態(tài)

  • company
  • director
  • contact

事件

  • next:按順序切換到下一個(gè)狀態(tài)。
  • prev:按順序切換到上一個(gè)狀態(tài)。

實(shí)現(xiàn)起來像下面這樣:

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' },
      },
    },
  }
})

現(xiàn)在讓我們來分析一下這段代碼。 createMachine 方法接受由 id、initial 和 states 共同組成的對(duì)象,這三個(gè)字端的作用分別是:

  • id:唯一標(biāo)識(shí)符。
  • initial:初始狀態(tài)
  • states:所有狀態(tài),其中的鍵是狀態(tài)的名稱,值是描述狀態(tài)的對(duì)象。

接下來我們?cè)俜治鲆幌?director 這個(gè)狀態(tài):

  • 它有一個(gè)名字,叫做 director。
  • 它可以對(duì)兩個(gè)事件做出反應(yīng):previous 事件,將狀態(tài)轉(zhuǎn)換為 company。next 事件,將狀態(tài)設(shè)置為 contacct。

使用 xstate 將有限狀態(tài)機(jī)可視化

感謝 xstate 的開發(fā)人員,我們可以將上面的代碼粘貼到 xstate 的在線可視化編輯器中。這個(gè)工具可以展示出有限狀態(tài)機(jī)的所有可能的狀態(tài)和事件。 我們的狀態(tài)機(jī)是這個(gè)樣子:

我承認(rèn)為了實(shí)現(xiàn)這樣一個(gè)簡(jiǎn)單的小功能編寫這么多代碼似乎有些過度設(shè)計(jì),但是我們繼續(xù)往下看,我保證你會(huì)相信使用有限狀態(tài)機(jī)是值得的。

通過 9 個(gè)步驟完成重構(gòu)

我們實(shí)現(xiàn)了有限狀態(tài)機(jī),但是我們還沒有做最重要的事情。我們必須重構(gòu)渲染邏輯。 接下來我要為有限狀態(tài)機(jī)實(shí)現(xiàn)一些上下文。

步驟1: 為上下文添加類型定義

type Context = {
  currentView: ReactNode;
}

步驟2: 添加將狀態(tài)映射到組件的函數(shù)

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

步驟3: 將上下文添加到有限狀態(tài)機(jī)的定義中

context: {
  currentView: <CompanyDataFormPart />,
}

步驟4: 定義一個(gè)將改變上下文的函數(shù)

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

步驟5: 將這個(gè)函數(shù)添加到有限狀態(tài)機(jī)的 actions 中

{
  actions: {
    changeComponent,
  }
}

步驟6: 將由 previous 和 next 事件觸發(fā)這個(gè)操作

{
  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 函數(shù)將事件發(fā)送到有限狀態(tài)機(jī)

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

步驟9: 渲染當(dāng)前狀態(tài)對(duì)應(yīng)的組件

{current.context.currentView}

我們馬上就要完成了!

有限狀態(tài)機(jī)的安全性,使得我們的表單同樣安全

我們?cè)倩貋砜纯粗芭e的最后一個(gè)反例。

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

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

可以看到,Step 和 Views 是解耦的。我們通過逐步渲染分頁(yè)進(jìn)度面板的值來進(jìn)行映射,并且使用當(dāng)前索引來渲染 Views 數(shù)組中的元素。 在我們的有限狀態(tài)機(jī)中如何用更好的方式來實(shí)現(xiàn)這一點(diǎn)? 我們首先來稍微改變一下上下文。

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

export type Context = {
  currentView: View;
}

接下來修改一下 mapStateToComponent 這個(gè)函數(shù),順便把函數(shù)名也改掉。

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

最后為我們的有限狀態(tài)機(jī)添加一些類型,將類型和 actions 移到不同的文件里。 現(xiàn)在我們的代碼像下面這樣: 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 中使用有限狀態(tài)機(jī)的概括

可能你會(huì)說,“它看起來仍然很復(fù)雜。”。但是請(qǐng)你記住,如果你正在為一家大公司研發(fā)一個(gè)非常重要的項(xiàng)目,那其中一點(diǎn)微小的錯(cuò)誤都可能會(huì)導(dǎo)致非常嚴(yán)重的資金損失。 最后我總結(jié)一下使用有限狀態(tài)機(jī)的幾個(gè)優(yōu)勢(shì):

  • 類型安全。我們永遠(yuǎn)都不會(huì)使用在類型定義中的狀態(tài)以外的狀態(tài),否則會(huì)編譯錯(cuò)誤。
  • 不會(huì)有錯(cuò)誤的狀態(tài)和錯(cuò)誤的轉(zhuǎn)換。如果不改變有限狀態(tài)機(jī)的定義,那么不可能有人能夠做到從第1步直接跳轉(zhuǎn)到第3步。
  • 所有的邏輯都在一個(gè)位置進(jìn)行描述。

原文鏈接:https://juejin.cn/post/7099666477208305671

欄目分類
最近更新