React使用有限狀態(tài)機的實現(xiàn)示例
在 React 中使用有限狀態(tài)機,似乎不是一個尋常的話題。因為有限狀態(tài)機通常和前端關(guān)系不大。 但是最近我發(fā)現(xiàn)了一個非常棒的技巧,可以在復(fù)雜的 React 項目中發(fā)揮有限狀態(tài)機的作用??梢院芎玫奶岣叱绦虻陌踩?。 下面我們就來看看吧。
什么是有限狀態(tài)機?
有限狀態(tài)機,英文是 Finite State Machine,簡稱 FSM,有時候也被稱為有限狀態(tài)自動機(Finite State AutoMation)。它是一種描述系統(tǒng)行為的數(shù)學(xué)計算模型,也就是一種抽象機。它可以替代圖靈機等其他類型的模型。 有限狀態(tài)機由給定對象的所有可能狀態(tài)以及它們之間的轉(zhuǎn)換組成。和圖靈機相比,它的計算能力較低。這種計算能力的區(qū)別意味著 FSM 的計算能力更加有限,因為它具有有限數(shù)量的狀態(tài),不如的話就是無限狀態(tài)機了。 更重要的是:狀態(tài)機的一條規(guī)則是:它在任何時候都只處于一種狀態(tài)。 由于有限狀態(tài)機會根據(jù)適當(dāng)?shù)慕M合或預(yù)定的時間順序產(chǎn)生某些動作,因此我們可以在現(xiàn)代社會的任何地方找到有限狀態(tài)機的影子。這些包括自動售貨機、電梯,甚至是紅綠燈。
有限狀態(tài)機的示例
一個現(xiàn)實中完美匹配有限狀態(tài)機的例子是紅綠燈。我們來分析一下紅綠燈的工作方式: 它有四種狀態(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)換。另外,紅綠燈在任何時候都只能處于一種狀態(tài)。這意味著我們在這處理的是有限狀態(tài)機。 更重要的是,通過實現(xiàn)有限狀態(tài)機,我們可以保證,模型不會發(fā)生意外。以紅綠燈為例,紅綠燈絕對不會出現(xiàn)直接從綠燈轉(zhuǎn)換成紅燈的情況。
有限狀態(tài)機和軟件開發(fā)、計算機科學(xué)有什么關(guān)系?
其實有很多關(guān)系。特別是游戲開發(fā),很多游戲中都會大量使用有限狀態(tài)機。 舉個例子,大家應(yīng)該都玩過超級馬里奧這款 2D 游戲。馬里奧在游戲里可以做什么呢? 他可以: 靜止、朝右走、朝左走、跳躍。 從代碼的角度來看,它對應(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)機呢?
無論是從上面的概念還是具體的場景,我都是想保證你對有限狀態(tài)機有一個了解。 接下來我來講講有限狀態(tài)機在前端的應(yīng)用場景。 首先我得承認(rèn),在前端開發(fā)中有限狀態(tài)機并不是那么常見。我認(rèn)為這個現(xiàn)象的主要原因是因為它不是實現(xiàn)功能最簡單也不是最快的方法。 這有點像 TypeScript,它會讓你慢一點,它會帶來一些復(fù)雜性。但最終每個人都會從中受益。 為了證明我的這種觀點并非毫無根據(jù),我會展示一個我曾經(jīng)開發(fā)的 React 項目中使用有限狀態(tài)機的示例。 這是一個很簡單的注冊表單,分為三個部分。每個部分都會根據(jù)當(dāng)前填寫的進度進行渲染。
React 應(yīng)用程序中的注冊表單的傳統(tǒng)實現(xiàn)方式
我先快速演示一下我實現(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> </>
這一切看上去似乎很正常,表單可以切換到下一個和上一個步驟。但這里存在一個很明顯的錯誤。 那就是程序沒有考慮邊界問題。這意味著 currentStep 的值可能超過最大步驟,也就是 2,也可能低于 0。 如果要修復(fù)它,我們會寫出下列代碼:
onPrevious={() => { setCurrentStep(prev => Math.max(prev - 1, 0)) }} onNext={()=>{ setCurrentStep(prev => Math.min(prev + 1, Views.length - 1)) }}
還有其他風(fēng)險
這個代碼運行起來確實沒有問題,但還是會有一些潛在風(fēng)險。 在軟件開發(fā)中,很少會出現(xiàn)你一個人負(fù)責(zé)整個項目的情況,一般來說是一整個團隊在協(xié)作,這也就意味著有許多其他開發(fā)人員會檢查你的代碼,并且會視圖理解它并可能會修改它。 我們假設(shè)有個人在表單頂部寫了一個方法,直接跳到了第三步。
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> ]
這些錯誤中的任何一個投入生產(chǎn)都可能會出現(xiàn)下面這種情況:
這似乎看上去不是什么大問題
也許確實不是什么大問題。但是你要知道,我的例子是很簡單的一個流程。在真實的項目中,會有更大更復(fù)雜的惡項目,例如用于銀行和匯款的金融類程序,用于審批和工作流的辦公類程序。 如果我們在一個地方定義所有可能出現(xiàn)的狀態(tài)和狀態(tài)之間的轉(zhuǎn)換,會不會更容易?類似于某種約定,我們可以很容易的查看其中的整個邏輯,并且確保不會發(fā)生其他任何約定之外的事情。 實現(xiàn)這種模式的那個東西就是有限狀態(tài)機。
把表單重構(gòu)為有限狀態(tài)機
首先,我們先來只關(guān)注 onNext 和 onPrevious 函數(shù)。我們想要制造一臺機器,我們用下面的狀態(tài)和事件來描述它的行為,也就是為這臺機器的特性設(shè)計一個模型。
狀態(tài)
- company
- director
- contact
事件
- next:按順序切換到下一個狀態(tài)。
- prev:按順序切換到上一個狀態(tài)。
實現(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 共同組成的對象,這三個字端的作用分別是:
- id:唯一標(biāo)識符。
- initial:初始狀態(tài)
- states:所有狀態(tài),其中的鍵是狀態(tài)的名稱,值是描述狀態(tài)的對象。
接下來我們再分析一下 director 這個狀態(tài):
- 它有一個名字,叫做 director。
- 它可以對兩個事件做出反應(yīng):previous 事件,將狀態(tài)轉(zhuǎn)換為 company。next 事件,將狀態(tài)設(shè)置為 contacct。
使用 xstate 將有限狀態(tài)機可視化
感謝 xstate 的開發(fā)人員,我們可以將上面的代碼粘貼到 xstate 的在線可視化編輯器中。這個工具可以展示出有限狀態(tài)機的所有可能的狀態(tài)和事件。 我們的狀態(tài)機是這個樣子:
我承認(rèn)為了實現(xiàn)這樣一個簡單的小功能編寫這么多代碼似乎有些過度設(shè)計,但是我們繼續(xù)往下看,我保證你會相信使用有限狀態(tài)機是值得的。
通過 9 個步驟完成重構(gòu)
我們實現(xiàn)了有限狀態(tài)機,但是我們還沒有做最重要的事情。我們必須重構(gòu)渲染邏輯。 接下來我要為有限狀態(tài)機實現(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)機的定義中
context: { currentView: <CompanyDataFormPart />, }
步驟4: 定義一個將改變上下文的函數(shù)
const changeComponent = assign<Context>({ currentViewe: (context, event, { action }) => { return mapStateToComponent[action.payload as string]; } })
步驟5: 將這個函數(shù)添加到有限狀態(tài)機的 actions 中
{ actions: { changeComponent, } }
步驟6: 將由 previous 和 next 事件觸發(fā)這個操作
{ 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)機
onPrevious={() => { send('previous'); }} onNext={() => { send('next'); }}
步驟9: 渲染當(dāng)前狀態(tài)對應(yīng)的組件
{current.context.currentView}
我們馬上就要完成了!
有限狀態(tài)機的安全性,使得我們的表單同樣安全
我們再回來看看之前舉的最后一個反例。
const Step = { Company: 0, Director: 1, Contact: 2, } as const; const Views = [ <CompanyDataFormPart />, <DirectorDataFormPart />, <ContactDataFormPart />, <div>I should not be there!</div> ]
可以看到,Step 和 Views 是解耦的。我們通過逐步渲染分頁進度面板的值來進行映射,并且使用當(dāng)前索引來渲染 Views 數(shù)組中的元素。 在我們的有限狀態(tài)機中如何用更好的方式來實現(xiàn)這一點? 我們首先來稍微改變一下上下文。
export type View = { Component: ReactNode; step: number; } export type Context = { currentView: View; }
接下來修改一下 mapStateToComponent 這個函數(shù),順便把函數(shù)名也改掉。
const mapStateToView: Record<string, View> = { company: { Component: <CompanyDataFormPart />, step: 0, }, director: { Component: <DirectorDataFormPart />, step: 1, }, contact: { Component: <ContactDataFormPart />, step: 2, }, };
最后為我們的有限狀態(tài)機添加一些類型,將類型和 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)機的概括
可能你會說,“它看起來仍然很復(fù)雜。”。但是請你記住,如果你正在為一家大公司研發(fā)一個非常重要的項目,那其中一點微小的錯誤都可能會導(dǎo)致非常嚴(yán)重的資金損失。 最后我總結(jié)一下使用有限狀態(tài)機的幾個優(yōu)勢:
- 類型安全。我們永遠都不會使用在類型定義中的狀態(tài)以外的狀態(tài),否則會編譯錯誤。
- 不會有錯誤的狀態(tài)和錯誤的轉(zhuǎn)換。如果不改變有限狀態(tài)機的定義,那么不可能有人能夠做到從第1步直接跳轉(zhuǎn)到第3步。
- 所有的邏輯都在一個位置進行描述。
到此這篇關(guān)于React使用有限狀態(tài)機的實現(xiàn)示例的文章就介紹到這了,更多相關(guān)React 有限狀態(tài)機內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
ReactiveCocoa代碼實踐之-UI組件的RAC信號操作
這篇文章主要介紹了ReactiveCocoa代碼實踐之-UI組件的RAC信號操作 的相關(guān)資料,需要的朋友可以參考下2016-04-04react+zarm實現(xiàn)底部導(dǎo)航欄的示例代碼
本文主要介紹了react?+?zarm?實現(xiàn)底部導(dǎo)航欄的示例代碼,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-05-05react使用useState修改對象或者數(shù)組的值無法改變視圖的問題
這篇文章主要介紹了react使用useState修改對象或者數(shù)組的值無法改變視圖的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-08-08react?card?slider實現(xiàn)滑動卡片教程示例
這篇文章主要為大家介紹了react?card?slider實現(xiàn)滑動卡片教程示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-09-09react18中react-redux狀態(tài)管理的實現(xiàn)
本文主要介紹了react18中react-redux狀態(tài)管理的實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05react-redux及redux狀態(tài)管理工具使用詳解
Redux是為javascript應(yīng)用程序提供一個狀態(tài)管理工具集中的管理react中多個組件的狀態(tài)redux是專門作狀態(tài)管理的js庫(不是react插件庫可以用在其他js框架中例如vue,但是基本用在react中),這篇文章主要介紹了react-redux及redux狀態(tài)管理工具使用詳解,需要的朋友可以參考下2023-01-01React中實現(xiàn)編輯框自動獲取焦點與失焦更新功能
在React應(yīng)用中,編輯框的焦點控制和數(shù)據(jù)回填是一個常見需求,本文將介紹如何使用useRef和useEffect鉤子,在組件中實現(xiàn)輸入框自動獲取焦點及失焦后更新數(shù)據(jù)的功能,文中通過代碼示例給大家講解的非常詳細(xì),需要的朋友可以參考下2024-01-01react?hooks?UI與業(yè)務(wù)邏輯分離必要性技術(shù)方案
這篇文章主要為大家介紹了react?hooks?UI與業(yè)務(wù)邏輯分離必要性技術(shù)方案詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-11-11