[note] State Machines
此篇為各筆記之整理,非原創內容,資料來源可見下方連結與文後參考資料:State Machines in JavaScript with XState, v2 @ Frontend Masters
TL;DR
State and Event
interface Typestate<TContext> {
value: StateValue; // 目前的 state
context: TContext;
event: TEvent; // 收到的事件
matches(parentStateValue): boolean; // 是否是屬於某個 parentState 底下的 state
can(event: TEvent): boolean; // 接收到該 event 某會不會有 transition
}
interface EventObject {
type: string;
}
Machine:
const machine = createMachine({
/*...*/
});
// 使用 transition 可以用來檢視從某 state 接收到特定 event 時會轉換成什麼 state
const nextState = machine.transition('playing', {
type: 'PAUSE',
});
Service:
// 使用 service 可以實際操作某個完整的狀態圖
const service = interpret(machine, { devTools: true }).start();
// 取得當前的 state
service.state;
// 使用 send 來轉換狀態
service.send({ type: 'LOADED' });
service.subscribe((state) => {
state.can({ type: 'PLAY' }); // 收到該 event 後會不會有對應的 transition
state.value;
});
Debug
可以使用 inspect 工具,它會在瀏覽器開啟另一個分頁,同步顯示當前的狀態,而且它是雙向的,也就是說:
- 從 App 改變狀態,inspector 的狀態也會改變
- 從 inspector 改變狀態,App 的狀態也會改變
import { interpret, inspect } from '@xstate/inspect';
inspect({
iframe: false,
url: 'https://stately.ai/viz?inspect',
});
const service = interpret(playerMachine, { devTools: true }).start();
Actions
Actions @ XState
Actions 是屬於 "fire-and-forget" 類型的事件(event),也就是 side-effect,事件會導致連帶執行的動作,可以在三個不同的事件點定義:
entry
exit
- transition (do)
Inline:使用 inline function 定義 action
const machine = createMachine({
initial: 'loading',
states: {
loading: {
// entry action
entry: [
{
type: 'loadData',
exec: () => console.log('Entry / Loading data'),
},
],
exit: [() => console.log('Exit / Loading data')],
on: {
LOADED: 'playing',
},
},
playing: {
on: {
PAUSE: {
// transition action
actions: [
{
type: 'pauseAudio',
exec: () => console.log('Pause audio!'),
},
],
target: 'paused',
},
},
},
paused: {
on: {
PLAY: 'playing',
},
},
},
});
雖然如果只有一個 action 的話,在 actions
後可直接放物件,但會建議統一放陣列,方便未來如果有其他 action 要執行的話,會比較好擴充。
Serialize:使用 config 的方式定義 actions
const machine = createMachine({
initial: 'loading',
states: {
loading: {
entry: ['loadData'],
on: {
LOADED: 'playing',
},
},
playing: {
on: {
PAUSE: {
actions: ['pauseAudio'],
target: 'paused',
},
},
},
paused: {
on: {
PLAY: 'playing',
},
},
},
})
.withConfig({
actions: {
loadData() {
console.log('configured load data');
},
pauseAudio() {
console.log('configured pause data');
},
},
});
建議一開始都把 action 放在 transition 上,也就是當事件發生時(LOADED)就做某個 action(例如,playingAudio)。但如果你發現在不同的 transition 都會用到相同的 action 時(例如,在 LOADED 和 PLAY 都會執行 playingAudio 的 action),就可以考慮把這個 Action 放到 entry 或 exit 來執行(例如,每次進到 playing 的狀態時,就執行 entry / playingAudio 的動作),如此就不需要重複定義相同的 action。
Actions 不會無緣無故發生,它一定是由於某個事件導致的。
補充:raise 的使用
如 果需要在一個 state machine 內部觸發另一個事件,可以使用 raise
:
const machine = createMachine({
// ...
states: {
loading: {
on: {
LOADED: {
// 當收到 LOADED 事件時,直接發送 PLAY 事件
actions: [raise('PLAY')], // 或 raise({type: 'PLAY'})
},
target: 'playing',
},
},
},
});
raise()
的使用蠻特別的,它直接就會回傳一個 action,所以不需要也「不能」在 function 中才執行它。
和 service.send()
不同,send 大部分是用來觸發「其他」 state machine 的事件。
Context
Context 裡會放的是不屬於 state machine 的其他狀態(extended state)。