XState有限狀態機JavaScript庫的概述
我曾經在過去的文章中提到過有關有限狀態機的內容,並提到過XState。在這篇文章中,我想要介紹這個受歡迎的JavaScript庫。
有限狀態機是一種處理複雜狀態和狀態變化並保持程式碼幾乎無錯誤的有趣方法。
就像我們使用各種工具來設計軟體項目以在構建之前設計它一樣,我們在構建之前使用模型和使用者體驗工具來思考UI,有限狀態機幫助我們解決狀態轉換的問題。
電腦程式就是在輸入之後從一個狀態過渡到另一個狀態的事情。如果不仔細注意,事情會失控,而XState是一個非常有幫助的工具,可以幫助我們處理不斷增長的狀態複雜性。
你可以使用npm安裝XState:
npm install xstate
然後,您可以使用ES模塊語法在程序中導入它。通常,您最少要導入Machine
和 interpret
函數:
import { Machine, interpret } from 'xstate'
在瀏覽器中,您還可以直接從CDN導入它:
<script src="https://unpkg.com/[[email protected]](/cdn-cgi/l/email-protection)/dist/xstate.js"></script>
這樣將在window
對象上創建一個全局的XState變量。
接下來,您可以使用Machine
工廠函數來定義一個有限狀態機。 這個函數接受一個配置對象,並返回一個對新創建的狀態機的引用:
const machine = Machine({
})
在配置中,我們傳遞一個標識狀態機的id
字符串,以及初始狀態字符串。 這是一個簡單的交通信號燈示例:
const machine = Machine({
id: 'trafficlights',
initial: 'green'
})
我們還傳遞一個包含允許的狀態的states
對象:
const machine = Machine({
id: 'trafficlights',
initial: 'green',
states: {
green: {
},
yellow: {
},
red: {
}
}
})
在這裡,我定義了三個狀態:green
、yellow
和red
。
要從一個狀態過渡到另一個狀態,我們將向機器發送一個消息,並根據我們設置的配置知道該做什麼。
在這裡,我們設置了當我們處於green
狀態並且收到TIMER
事件時,切換到yellow
狀態:
const machine = Machine({
id: 'trafficlights',
initial: 'green',
states: {
green: {
on: {
TIMER: 'yellow'
}
},
yellow: {
},
red: {
}
}
})
我稱之為TIMER
,因為交通信號燈通常有一個簡單的計時器,在每X秒改變燈的狀態。
現在,讓我們填充其他2個狀態轉換:我們從黃色轉換到紅色,然後從紅色轉換到綠色:
const machine = Machine({
id: 'trafficlights',
initial: 'green',
states: {
green: {
on: {
TIMER: 'yellow'
}
},
yellow: {
on: {
TIMER: 'red'
}
},
red: {
on: {
TIMER: 'green'
}
}
}
})
我們如何觸發過渡呢?
您可以使用以下方式獲取機器的初始狀態字符串表示:
machine.initialState.value //'green'在這個例子中
並使用machine
的transition()
方法(Machine()
返回的狀態機實例)切換到新的狀態:
const currentState = machine.initialState.value
const newState = machine.transition(currentState, 'TIMER')
您可以將新的狀態物件存儲到變量中,並且可以通過訪問value
屬性來獲取其字符串表示:
const currentState = machine.initialState.value
const newState = machine.transition(currentState, 'TIMER')
console.log(newState.value)
使用transition()
方法,您必須始終跟踪當前狀態,在我看來這有點痛苦。如果我們可以問機器它的當前狀態的話就太好了。
這是通過創建一個狀態圖來完成的,在XState中稱為服務。我們這樣做是通過調用從xstate
導入的interpret()
方法並傳遞給它狀態機物件,然後調用start()
來啟動服務:
const toggleService = interpret(machine).start()
現在,我們可以使用此服務的send()
方法檢索新的狀態,而不需要像使用machine.transition()
那樣傳遞當前狀態:
const toggleService = interpret(machine).start()
toggleService.send('TOGGLE')
我們可以將返回值存儲下來,該返回值將保存新的狀態:
const newState = toggleService.send('TOGGLE')
console.log(newState.value)
這只是XState的皮毛。
給定一個狀態,您可以通過其nextEvents
屬性知道將觸發狀態變更的事件,它將返回一個數組。
是的,因為從一個狀態可以轉換到多個狀態,具體取決於您得到的觸發器。
在交通信號燈的情況下,這不是會發生的情況,但我們來模擬上一篇有限狀態機文章中提到的家庭燈的例子:
當您進入屋內時,您可以按下其中的2個按鈕之一,p1或p2。當您按下這些按鈕時,l1燈會打開。
想像這是入口燈,您可以脫下外套。完成後,您可以決定您想要進入的房間(例如廚房或臥室)。
如果您按下p1按鈕,l1將關閉並且l2將打開。而如果您按下p2按鈕,l1將關閉並且l3將打開。
再次按下其中2個按鈕之一,p1或p2,目前打開的燈將關閉,並且我們將回到系統的初始狀態。
這是我們的XState狀態機物件:
const machine = Machine({
id: 'roomlights',
initial: 'nolights',
states: {
nolights: {
on: {
p1: 'l1',
p2: 'l1'
}
},
l1: {
on: {
p1: 'l2',
p2: 'l3'
}
},
l2: {
on: {
p1: 'nolights',
p2: 'nolights'
}
},
l3: {
on: {
p1: 'nolights',
p2: 'nolights'
}
},
}
})
現在,我們可以創建一個服務並向其發送消息:
const toggleService = interpret(machine).start();
toggleService.send('p1').value //'l1'
toggleService.send('p1').value //'l2'
toggleService.send('p1').value //'nolights'
這裡有一件事我們缺少的是當我們切換到新的狀態時要做些什麼。可以通過在我們傳遞給Machine()
工廠函數的第二個對象參數中定義的操作來實現這一點:
const machine = Machine({
id: 'roomlights',
initial: 'nolights',
states: {
nolights: {
on: {
p1: {
target: 'l1',
actions: 'turnOnL1'
},
p2: {
target: 'l1',
actions: 'turnOnL1'
}
}
},
l1: {
on: {
p1: {
target: 'l2',
actions: 'turnOnL2'
},
p2: {
target: 'l3',
actions: 'turnOnL3'
}
}
},
l2: {
on: {
p1: {
target: 'nolights',
actions: ['turnOffAll']
},
p2: {
target: 'nolights',
actions: ['turnOffAll']
}
}
},
l3: {
on: {
p1: {
target: 'nolights',
actions: 'turnOffAll'
},
p2: {
target: 'nolights',
actions: 'turnOffAll'
}
}
},
}
}, {
actions: {
turnOnL1: (context, event) => {
console.log('turnOnL1')
},
turnOnL2: (context, event) => {
console.log('turnOnL2')
},
turnOnL3: (context, event) => {
console.log('turnOnL3')
},
turnOffAll: (context, event) => {
console.log('turnOffAll')
}
}
})
注意,現在在傳遞給on
的對象中,每個狀態轉換不再只是一個字符串,而是一個具有target
屬性(在其中傳遞我們之前使用的字符串)的對象,並且我們還有一個actions
屬性,可以設置要運行的操作。
我們可以通過傳遞字符串的數組而不是字符串來運行多個操作。
並且你還可以直接在actions
屬性上定義操作,而不是將它們“集中”到單獨的對象中:
const machine = Machine({
id: 'roomlights',
initial: 'nolights',
states: {
nolights: {
on: {
p1: {
target: 'l1',
actions: (context, event) => {
console.log('turnOnL1')
},
...
但在這種情況下,將它們放在一起更方便,因為不同的狀態轉換觸發了相似的操作。
這就是本教程的全部內容。我建議您查看XState Docs以獲取更高級用法的XState,但這是一個開始。