跳至主要内容

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

raise() 的使用蠻特別的,它直接就會回傳一個 action,所以不需要也「不能」在 function 中才執行它。

提示

service.send() 不同,send 大部分是用來觸發「其他」 state machine 的事件。

Context

Context 裡會放的是不屬於 state machine 的其他狀態(extended state)。

Giscus