还记得在 Day 15 马克杯 装水 Guard 的例子吗?
另外一组例子,一个水瓶或是马克杯的状态
一开始是『空的』,有「装水」这个事件,每次要装水时,都会当前水量 + 预计倒水量,有没有超过容器最大容积
「装水」可以一次装满,转移到『满水位』的状态;或是每次都装一点,转移到『有部份水量』的状态
但每次「装水」事件都会检查容量够不够。
最後『满水位』时,配上「封盖」的事件,可以把我们的水瓶或者是马克杯『盖上』
今天我们就来实作这个状态机吧!
TLDR CodeSandbox Demo
按理我们会先思考规划出上面的状态图,接着再将状态图实作成为程序码。
已经有状态图了所以来想想实作的部分!
暂定命名为「空杯」、「半满」、「满水」、「盖上」来对应上述英文的状态
const containerMachineConfig = {
id: "马克杯",
initial: "空杯",
states:{
空杯:{},
半满:{},
满水:{},
盖上:{}
}
}
由上图可看出这个状态机满单纯的,只有两种事件!
就是『加水』跟『封盖』
const containerMachineConfig = {
id: "马克杯",
initial: "空杯",
context: { 容量: 300, 当前水量: 0 },
states:{
+ 空杯:{on:{加水:...}},
+ 半满:{on:{加水:...}},
+ 满水:{on:{封盖:...}},
+ 盖上:{type: "final"} // 最终状态
}
}
为了实作事件,我们继续往下思考
这个马克杯的状态机有哪些资料需要被储存起来、共享,也就是 context 需要什麽?
const containerMachineConfig = {
id: "马克杯",
initial: "空杯",
+ context: { 容量: 300, 当前水量: 0 },
...
如何实作加水这件事呢?想必『加水』这个事件,会将使用者倒入(输入)的水量做验证并且存进 context。
如何将使用者输入带进来状态机呢?以 redux 假设,我们可以发起一个 redux action ,这个 action 会指名 type ,让 reducer 判别进行什麽状态处理,并透过 payload 将额外的资料。
// 自己手写 action
{ type: 'ADD_TODO', text: 'Go to swimming pool' }
{ type: 'TOGGLE_TODO', index: 1 }
{ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }
或
// 使用 Redux Toolkit 的 action creator 及 其他附加功能
{ type: 'ADD_TODO', payload: { text: 'Go to swimming pool' } }
在 XState 当中,想将使用者额外的资料带进去 Machine 里,也可以一起放在 event object 内。
- service.send({ type: "加水"});
+ service.send({ type: "加水", 加水量: 30 });
这样子在撰写 Machine config 时,就能透过这个额外的 key 决定要对使用者额外的资料做什麽处理!
在这边也跟读者说声不好意思,我忽然发现前面解释 XState 中的「事件」没有说得很仔细。
接着我们回去处理『加水』事件本身。依照状态图,「空杯」+『加水』可能转换到两种状态 「半满」or「满水」,所以『加水』事件的转移要使用 Array [],储存多种转移的可能!
- 空杯:{on:{加水:...}},
+ 空杯:{on:{加水:[]}},
「空杯」+『加水』可能转换到两种状态 「半满」or「满水」
- 空杯:{on:{加水:[]}},
+ 空杯:{on:{加水:[{ target: "满水" }, { target: "半满" }]}},
「半满」or「满水」都要更新 context 里的 当前水量。
为了更新 context ,我们必须使用 XState 的 side effect -> actions,并透过 assign
API 来更新 context
- 空杯:{on:{加水:[{ target: "满水" }, { target: "半满" }]}},
+ 空杯: {
+ on: {
+ 加水: [
+ { // 初始的 当前水量 是 0 ,直接拿 event 的 加水量 即可
+ actions: [
+ assign({ 当前水量: (context, event) => event["加水量"] })
+ ],
+ target: "满水",
+ },
+ { // 初始的 当前水量 是 0 ,直接拿 event 的 加水量 即可
+ actions: [
+ assign({ 当前水量: (context, event) => event["加水量"] })
+ ],
+ target: "半满",
+ },
+ ]
+ }
+ }
为了决定该从「空杯」转换成「半满」还是「满水」或者是停留在「空杯」不动
我们需要透过在 event 的转换描述中使用 key cond
,保护转换的进行
空杯: {
on: {
加水: [
{ // 初始的 当前水量 是 0 ,直接拿 event 的 加水量 即可
actions: [
assign({ 当前水量: (context, event) => event["加水量"] })
],
target: "满水",
+ // 加超过杯子的容量,直接进入满水位
+ cond: (context, event) => event["加水量"] >= context["容量"],
},
{ // 初始的 当前水量 是 0 ,直接拿 event 的 加水量 即可
actions: [
assign({ 当前水量: (context, event) => event["加水量"] })
],
target: "半满",
+ // 未超过杯子的容量,进入半满水位
+ cond: (context, event) => event["加水量"] < context["容量"],
},
]
}
}
这边直接在 cond
後面写入 Guards Condition Functions (Predicate callback) ,而不是外挂在 createMachine(machineConfig,{ guards:{ guard1,guard2} })
是比较偷懒的方法。
XState 也提供这样的方式,让我们能快速测试原型、验证想法,但等到开发中後期,还是建议将 Guards Condition Functions ( Predicate callback ) 写进 const someMachine = createMachine(machineConfig,extraOptions)
的 extraOptions 当中。这样子我们比较好 除错、测试以及更漂亮的呈现在 Visualizer 上。
Refactoring inline guard implementations in the guards property of the machine options makes it easier to debug, serialize, test, and accurately visualize guards.
XState - Guards Condition Functions
由於 cond
的判别式都已经呈现在一开始的状态图了,因此我们就同理类推「半满」的状态转换
半满: {
on: {
加水: [
{ // 把杯子已有的 当前水量 加上 使用者倒入的 加水量
actions: [
assign({
当前水量: (context, event) =>
context["当前水量"] + event["加水量"]
})
],
target: "满水",
// 加超过杯子的容量,直接进入满水位
cond: (context, event) =>
event["加水量"] + context["当前水量"] >= context["容量"]
},
{ // 把杯子已有的 当前水量 加上 使用者倒入的 加水量
actions: [
assign({
当前水量: (context, event) =>
context["当前水量"] + event["加水量"]
})
],
target: "半满"
}
]
}
},
最後最後,我们可以完成以下状态机
cond
中,有满多类似的 Guards Condition Functions!诚如官方推荐,我们可以将其抽象化、放入 extraOptions 中的 guards ,用 字串 在 machineConfig 里描述。https://xstate.js.org/docs/guides/guards.html
<<: [Day28] - Django-REST-Framework API 期末专案实作 (三)
今天我们接续昨天的话题,继续来聊聊AI领域里面比较有趣的一些演算法。 蚂蚁演算法 (Ant Colo...
All life is an experiment. The more experiments y...
从Sprite_Damage开始 写一个方法 接着是Sprite_Character 在Action...
为何会需要 .gitignore ? 常用的情况如下: 是否常常在 commit 档案时,会发现有一...
前言 本文说明使用scrapy爬虫函式库抓取海运FBX指数。 波罗的海货柜运价指数[FBX] 波罗的...