目前來説,無論是 to c 業務,還是 to b 業務,對於前端開發者的要求越來越高,各種絢麗的視覺效果,複雜的業務邏輯層出不窮。針對於業務邏輯而言,貫穿後端業務和前端交互都有一個關鍵點 —— 狀態轉換。
當然了,這種代碼實現本身並不複雜,真正的難點在於如何快速的進行代碼的修改。
在實際開發項目的過程中,ETC 原則,即 Easier To Change,易於變更是非常重要的。為什麼解耦很好? 為什麼單一職責很有用? 為什麼好的命名很重要?因為這些設計原則讓你的代碼更容易發生變更。ETC 甚至可以説是其他原則的基石,可以説,我們現在所作的一切都是為了更容易變更!!特別是針對於初創公司,更是如此。
例如:項目初期,當前的網頁有一個模態框,可以進行編輯,模態框上有兩個按鈕,保存與取消。這裏就涉及到模態框的顯隱狀態以及權限管理。隨着時間的推移,需求和業務發生了改變。當前列表無法展示該項目的所有內容,在模態框中我們不但需要編輯數據,同時需要展示數據。這時候我們還需要管理按鈕之間的聯動。僅僅這些就較為複雜,更不用説涉及多個業務實體以及多角色之間的細微控制。
重新審視自身代碼,雖然之前我們做了大量努力利用各種設計原則,但是想要快速而安全的修改散落到各個函數中的狀態修改,還是非常浪費心神的,而且還很容易出現“漏網之魚”。
這時候,我們不僅僅需要依靠自身經驗寫好代碼,同時也需要一些工具的輔助。
有限狀態機
有限狀態機是一個非常有用的數學計算模型,它描述了在任何給定時間只能處於一種狀態的系統的行為。當然,該系統中只能夠建立出一些有限的、定性的“模式”或“狀態” ,並不描述與該系統相關的所有(可能是無限的)數據。例如,水可以是四種狀態中的一種: 固體(冰)、液體、氣體或等離子體。然而,水的温度可以變化,它的測量是定量的和無限的。
總結來説,有限狀態機的三個特徵為:
- 狀態總數(state)是有限的。
- 任一時刻,只處在一種狀態之中。
- 某種條件下,會從一種狀態轉變(transition)到另一種狀態。
在實際開發中,它還需要:
- 初始狀態
- 觸發狀態變化的事件和轉換函數
- 最終狀態的集合(有可能是沒有最終狀態)
先看一個簡單的紅綠燈狀態轉換:
const light = {
currentState: 'green',
transition: function () {
switch (this.currentState) {
case "green":
this.currentState = 'yellow'
break;
case "yellow":
this.currentState = 'red'
break;
case "red":
this.currentState = 'green'
break;
default:
break;
}
}
}
有限狀態機在遊戲開發中大放異彩,已經成為了一種常用的設計模式。用這種方式可以使每一個狀態都是獨立的代碼塊,與其他不同的狀態分開獨立運行,這樣很容易檢測遺漏條件和移除非法狀態,減少了耦合,提升了代碼的健壯性,這麼做可以使得遊戲的調試變得更加方便,同時也更易於增加新的功能。
對於前端開發來説,我們可以從其他工程領域中多年使用的經驗學習與再創造。
XState 體驗
實際上開發一個 簡單的狀態機並不是特別複雜的事情,但是想要一個完善,實用性強,還具有可視化工具的狀態機可不是一個簡單的事。
這裏我要推薦 XState,該庫用於創建、解釋和執行有限狀態機和狀態圖。
簡單來説:上述的代碼可以這樣寫。
import { Machine } from 'xstate'
const lightMachine = Machine({
// 識別 id, SCXML id 必須唯一
id: 'light',
// 初始化狀態,綠燈
initial: 'green',
// 狀態定義
states: {
green: {
on: {
// 事件名稱,如果觸發 TIMRE 事件,直接轉入 yellow 狀態
TIMRE: 'yellow'
}
},
yellow: {
on: {
TIMER: 'red'
}
},
red: {
on: {
TIMER: 'green'
}
}
}
})
// 設置當前狀態
const currentState = 'green'
// 轉換的結果
const nextState = lightMachine.transition(currentState, 'TIMER').value
// => 'yellow'
// 如果傳入的事件沒有定義,則不會發生轉換,如果是嚴格模式,將會拋出錯誤
lightMachine.transition(currentState, 'UNKNOWN').value
其中 SCXML 是狀態圖可擴展標記語言, XState 遵循該標準,所以需要提供 id。當前狀態機也可以轉換為 JSON 或 SCXML。
雖然 transition 是一個純函數,非常好用,但是在真實環境使用狀態機,我們還是需要更強大的功能。如:
- 跟蹤當前狀態
- 執行副作用
- 處理延遲過度以及時間
- 與外部服務溝通
XState 提供了 interpret 函數,
import { Machine,interpret } from 'xstate'
// 。。。 lightMachine 代碼
// 狀態機的實例成為 serivce
const lightService = interpret(lightMachine)
// 當轉換時候,觸發的事件(包括初始狀態)
.onTransition(state => {
// 返回是否改變,如果狀態發生變化(或者 context 以及 action 後文提到),返回 true
console.log(state.changed)
console.log(state.value)
})
// 完成時候觸發
.onDone(() => {
console.log('done')
})
// 開啓
lightService.start()
// 將觸發事件改為 發送消息,更適合狀態機風格
// 初始化狀態為 green 綠色
lightService.send('TIMER') // yellow
lightService.send('TIMER') // red
// 批量活動
lightService.send([
'TIMER',
'TIMER'
])
// 停止
lightService.stop()
// 從特定狀態啓動當前服務,這對於狀態的保存以及使用更有作用
lightService.start(previousState)
我們也可以結合其他庫在 Vue React 框架中使用,僅僅只用幾行代碼就實現了我們想要的功能。
import lightMachine from '..'
// react hook 風格
import { useMachine } from '@xstate/react'
function Light() {
const [light, send] = useMachine(lightMachine)
return <>
// 當前狀態 state 是否是綠色
<span>{light.matches('green') && '綠色'}</span>
// 當前狀態的值
<span>{light.value}</span>
// 發送消息
<button onClick={() => send('TIMER')}>切換</button>
</>
}
當前的狀態機也是還可以進行嵌套處理,在紅燈狀態下添加人的行動狀態。
import { Machine } from 'xstate';
const pedestrianStates = {
// 初識狀態 行走
initial: 'walk',
states: {
walk: {
on: {
PED_TIMER: 'wait'
}
},
wait: {
on: {
PED_TIMER: 'stop'
}
},
stop: {}
}
};
const lightMachine = Machine({
id: 'light',
initial: 'green',
states: {
green: {
on: {
TIMER: 'yellow'
}
},
yellow: {
on: {
TIMER: 'red'
}
},
red: {
on: {
TIMER: 'green'
},
...pedestrianStates
}
}
});
const currentState = 'yellow';
const nextState = lightMachine.transition(currentState, 'TIMER').value;
// 返回級聯對象
// => {
// red: 'walk'
// }
// 也可以寫為 red.walk
lightMachine.transition('red.walk', 'PED_TIMER').value;
// 轉化後返回
// => {
// red: 'wait'
// }
// TIMER 還可以返回下一個狀態
lightMachine.transition({ red: 'stop' }, 'TIMER').value;
// => 'green'
當然了,既然有嵌套狀態,我們還可以利用 type: 'parallel' ,進行串行和並行處理。
除此之外,XState 還有擴展狀態 context 和過度防護 guards。這樣的話,更能夠模擬現實生活
// 是否可以編輯
functions canEdit(context: any, event: any, { cond }: any) {
console.log(cond)
// => delay: 1000
// 是否有某種權限 ???
return hasXXXAuthority(context.user)
}
const buttonMachine = Machine({
id: 'buttons',
initial: 'green',
// 擴展狀態,例如 用户等其他全局數據
context: {
// 用户數據
user: {}
},
states: {
view: {
on: {
// 對應之前 TIMRE: 'yellow'
// 實際上 字符串無法表達太多信息,需要對象表示
EDIT: {
target: 'edit',
// 如果沒有該權限,不進行轉換,處於原狀態
// 如果沒有附加條件,直接 cond: searchValid
cond: {
type: 'searchValid',
delay: 3
}
},
}
}
}
}, {
// 守衞
guards: {
canEdit,
}
})
// XState 給予了更加合適的 API 接口,開發時候 Context 可能不存在
// 或者我們需要在不同的上下文 context 中複用狀態機,這樣代碼擴展性更強
const buttonMachineWithDelay = buttonMachine.withContext({
user: {},
delay: 1000
})
// withContext 是直接替換,不進行淺層合併,但是我們可以手動合併
const buttonMachineWithDelay = buttonMachine.withContext({
...buttonMachine.context,
delay: 1000
})
我們還可以通過瞬時狀態來過度,瞬態狀態節點可以根據條件來確定機器應從先前的狀態真正進入哪個狀態。瞬態狀態表現為空字符串,即 '',如
const timeOfDayMachine = Machine({
id: 'timeOfDay',
// 當前不知道是什麼狀態
initial: 'unknown',
context: {
time: undefined
},
states: {
// Transient state
unknown: {
on: {
'': [
{ target: 'morning', cond: 'isBeforeNoon' },
{ target: 'afternoon', cond: 'isBeforeSix' },
{ target: 'evening' }
]
}
},
morning: {},
afternoon: {},
evening: {}
}
}, {
guards: {
isBeforeNoon: //... 確認當前時間是否小於 中午
isBeforeSix: // ... 確認當前時間是否小於 下午 6 點
}
});
const timeOfDayService = interpret(timeOfDayMachine
.withContext({ time: Date.now() }))
.onTransition(state => console.log(state.value))
.start();
timeOfDayService.state.value
// 根據當前時間,可以是 morning afternoon 和 evening,而不是 unknown 轉態
到這裏,我覺得已經介紹 XState 很多功能了,篇幅所限,不能完全介紹所有功能,不過當前的功能已經足夠大部分業務需求使用了。如果有其他更復雜的需求,可以參考 XState 文檔。
這裏列舉一些沒有介紹到的功能點:
- 進入和離開某狀態觸發動作(action 一次性)和活動(activity 持續性觸發,直到離開某狀態)
- 延遲事件與過度 after
- 服務調用 invoke,包括 promise 以及 兩個狀態機之間相互交互
- 歷史狀態節點,可以通過配置保存狀態並且回退狀態
當然了,對比於 x-state 這種,還有其他的狀態機工具,如 javascript-state-machine , Ego 等。大家可以酌情考慮使用。
總結
對於現代框架而言,無論是如火如荼的 React Hook 還是漸入佳境的 Vue Compoistion Api,其本質都想提升狀態邏輯的複用能力。但是考慮大部分場景下,狀態本身的切換都是有特定約束的,如果僅僅靠良好的編程習慣,恐怕還是難以寫出抑鬱修改的代碼。而 FSM 以及 XState 無疑是一把利器。
鼓勵一下
如果你覺得這篇文章不錯,希望可以給與我一些鼓勵,在我的 github 博客下幫忙 star 一下。
博客地址
參考
XState 文檔
JavaScript與有限狀態機