狀態管理與 XState

2020-03-18#front-end
文章目錄

如果現階段剛踏入前端的話,第一次接觸到狀態可能已經是在狀態管理體系已經很成熟的時候了。

狀態管理裡面的各種議題被打包成一系列的技術棧,以自己最熟悉的 React - Redux - Redux Observable 方案來說,分別就在處理狀態轉 UI、跨元件與把控狀態流的狀態管理到非同步狀態這些都會被稱作狀態管理工具,這讓我開始思考什麼是狀態?以及 xState 的應該用在哪裡?

XState

如果直接進 XState Document,xState 定義自己為一個實作有限狀態機的 Library 並同時以 Actor Model 管理一些並行操作:

有限狀態機

看他定義限狀態機會看到這五行定義:

  1. A finite number of states
  2. A finite number of events
  3. An initial state
  4. A transition function that determines the next state given the current state and event
  5. A (possibly empty) set of final states

不妨想像成小時候玩的跳格子,每個格子是一個狀態(State),我們在裡面都會從第一格開始(Initial State)接著丟沙包就像是個事件(Event),這個事件發生之後,我們要跳到別的格子這是轉換函式(Transition)。

當然從上可以發現 State Machine 只是一個抽象概念,但這個概念更具體要有哪些內容呢?Statecharts 就是 1987 年 David Harel 對有限狀態機提出的形式規範,他規範了要表達有限狀態機可以透過下列這些內容來表現:

  1. Guarded transitions
  2. Actions (entry, exit, transition)
  3. Extended state (context)
  4. Orthogonal (parallel) states
  5. Hierarchical (nested) states
  6. History

什麼是狀態?

狀態代表的是現在、當下系統的狀況,就好像我們說一個人的狀態不好,可能指得是他身體不好、心情不好、到經濟狀況不好都可以是一個人的狀態不好的範疇。而在網站前端中,以我們最常舉的 todo list 為例,有哪些東西可以是狀態的部分,例如:資料狀況,像是那些 todos 的資料;例如:UI 狀態,像是已完成、未完成;例如:使用者狀態,像是登入、非登入;例如:資料傳遞的狀態,像是:讀取中、讀取失敗;甚至到我們所處的頁面位址等等,這些都是狀態的一環。

而隨著使用者體驗的要求增加,對於每一種狀態都有更細緻的設計差異,所以對於資料不同會有更細緻的轉換,像是:數字在什麼區間要有更細緻的不同顏色、不同的空白狀態都要有對應的 UI 或互動、不同種類的狀態之間排列組合都會有不同的體驗發生在不同的元件上,所以在狀態的判斷與對應處理上越來越複雜。

但越深入狀態管理對於這些狀態就有越多疑惑:

狀態就是 UI 嗎?

在前端框架裡面,透過對 State 的觀察來更新 UI,這讓我們有時候會認為狀態的切換是跟隨著 UI 一起,但狀態其實不一定要跟 UI 綁在一起。舉例來說:一個系統的啟動到進入 Default 狀態中間可能系統有多個狀態的轉變,但 UI 這邊都不一定有變化。

相應的,就算 UI 有變化我們也不一定要將每一個變化都切成一個狀態,舉例來說:我們可以將一整個區塊的所有變動階段當成一個狀態。所謂的 UI 變化,其實是狀態邏輯的副作用,與狀態是分離的。

狀態就是數據嗎?

另一點所有會影響到狀態的內容都會被放在 state / store 之中,舉例來說:todo l

ist 的 todo 與 todo list 有沒有登入都會被放在 state 裡面,但其實兩個東西很不一樣。前者是系統用來計算、或陳述的資料,後者是一個驅動 UI 或階段的資料。舉例來說:前者就像是攝氏 28 度、後者可能就是冷或熱,後者被抽象、歸類過,並不是一個絕對且無限多的資料。

所以讓我們回到 todo list,我們過去在大多數的框架中,讓兩種內容都放在一起管理:

const state = {
    data: todo,
    isComplete: todo.length === 0,
    isOver: todo.filter(item => new Date(item.date) < Date.now())
    ...
}

但隨著後者階段資料越來越多,讓我們很難縱觀所有的狀態,且狀態之間的關聯性也無法表達出來,導致狀態判斷變ㄓ得很複雜。

// 多狀態判斷
if (isComplete && isOver && isLogin .... ) {...}

// 狀態下的狀態判斷
if (isComplete) {
    if (isOver) { ... }
    if (isLogin) { ... }
}

所以綜合以上 xState 在 Document 的 Concept 裡面就定義了:

State 代表的是透過狀態機建立出來有限、質性的系統模式,不是表述所無限的系統數據,我們原本的數據屬於 Statechart 之中提到的 Extended state (context)。

實作有限狀態機

不過今天不需要 xState 其實也可以非常直覺的透過原生 Javascript 來實作一個有限狀態機,以跳格子為例:

先利用一個 switch case 的判斷來進行狀態的轉換判斷。

function transition(state, event) {
	switch (state) {
		// 如果我在第一個格子丟沙包,我就到第二個格子,至於做了其他事情就留在原地。
		case 'BLOCK1':
		switch (event) {
			case 'THROW':
				return 'BLOCK2';
			default:
				return state;
		}
		case 'BLOCK2':
			switch (event) {
				case 'THROW':
					return 'BLOCK3';
				default:
					return state;
		}
		case 'BLOCK3':
			switch (event) {
				case 'THROW':
					return 'BLOCK3';
				default:
					return state;
		}
		default:
			return state;
	}
}

接著設置初始狀態並且新增一個 transition function 來進行狀態轉換,這樣就可以觸發特定動作來進行狀態機的狀態切換。

// 現在我在第一格
let currentState = 'BLOCK1';
function send(event) {
	let currentState = transition(currentState, event);
	console.log(currentState);
}
send('THROW');

但這樣都寫在 transition 裡面有點亂,我們可以把狀態邏輯單純抽出以純 Object 來管理:

const machine = {
	initial: 'BLOCK1',
	states: {
		BLOCK1: {
			on: {
				THROW: 'BLOCK2',
			},
		},
		BLOCK2: {
			on: {
				THROW: 'BLOCK3',
			},
		},
		BLOCK3: {
			on: {
				THROW: 'BLOCK3',
			},
		},
	},
};

接著修改 Transition Function 改為透過比對 Object Key 的方式來進行狀態切換。

function transition(state, event) {
	const nextState = machine.states[state].on?.[event] || state;
	return nextState;
}
let currentState = machine.initial;
function send(event) {
	let currentState = transition(currentState, event);
	console.log(currentState);
}
send('THROW');

這樣就是一個基礎的有限狀態機了,而實際上使用 xState 就跟上述的拆分過程一樣,只是 xState 替我們把這些底層跟 state charts 一些細節的規範打包好,並提供監聽的 state machine 的 service 的 Library,一個最簡單的使用範例如下:

import { createMachine, interpret } from 'xstate';

const machine = createMachine({
	initial: 'BLOCK1',
	states: {
		BLOCK1: {
			on: {
				THROW: 'BLOCK2',
			},
		},
		BLOCK2: {
			on: {
				THROW: 'BLOCK3',
			},
		},
		BLOCK3: {
			on: {
				THROW: 'BLOCK3',
			},
		},
	},
});

const service = interpret(machine);
service.onTransition(state => {
	console.log(state);
});
service.start();

小結

結束了狀態跟狀態機的介紹,接下來希望介紹更多關於 xState 的設計細節以及狀態機的其他應用,其他推廣狀態機在前端的使用情境。

參考資料

其它文章


© 2020 minw Powered by Gatsby