runed 是一個 Svelte 的 utility library ,它在 rune 的基礎上增加許多實用的功,今天就來介紹其中幾個我覺得比較酷的功能。
開始前請先安裝這個 package
pnpm add runed
# npm install runed
# yarn add runed
之前有提過 $effect
會自動追蹤依賴進而知道何時該重新執行 effect ,如果我們不想要它自動追蹤,那我們就必須要手動的加上 untrack
。
但就是有時候我們真的在眾多 state 中只是想追縱特定的 state 更新時再去執行 effect,但我又同時需要其他 state 的值。
watch的基本用法是:
import { watch } from "runed";
let count = $state(0);
watch(() => count, () => {
console.log(count);
}
);
watch
第一個參數是一個匿名函式回傳要監聽的值,第二個參數則是要執行的 effect
像是以下的例子,使用了兩個 $state
,但 watch
只監聽了 countOne
<script lang="ts">
import { watch } from 'runed';
let countOne = $state(0);
let countTwo = $state(0);
watch(
() => countOne,
() => {
console.log('countOne', countOne, 'countTwo', countTwo);
}
);
</script>
<div class="flex gap-12">
<button class="btn" onclick={() => countOne++}>Count One {countOne}</button>
<button class="btn" onclick={() => countTwo++}>Count Two {countTwo}</button>
</div>
相比於 $effect
來說不用特別使用 untrack(() => countTwo)
,就可以達到「只有 countOne
有更新才執行 effect 」這件事情。
StateHistory
正如它的名字就是可以記錄一個 state 的歷史紀錄,而且它同時封裝的 canUndo
、 canRedo
、 undo
及 redo
等 api,這點真的蠻方便的。
<script lang="ts">
import { StateHistory } from 'runed';
let countOne = $state(0);
const history = new StateHistory(
() => countOne,
(c) => (countOne = c)
);
</script>
<div>
<button class="btn" disabled={!history.canUndo} onclick={() => history.undo()}>Undo</button>
<button class="btn" disabled={!history.canRedo} onclick={() => history.redo()}>Redo</button>
</div>
<div>
{#each history.log as { snapshot, timestamp }}
<div class="flex gap-4">
<div>
value: {snapshot}
</div>
<div>
{new Date(timestamp)}
</div>
</div>
{/each}
</div>
應該看得出來它就是能夠幫助我們建立有限狀態機(FSM),稍微解釋一下 FSM ,就是一個描述一個在有限個數的狀態下各個狀態互相轉移的模型。
像是如果我要描述一個開關在 FSM 中我可以這樣描述
當前狀態 | 事件 | 下一狀態 |
---|---|---|
OFF | 按下開關 | ON |
ON | 按下開關 | OFF |
也就是我當前狀態如果是 OFF 那我按下開關時就會把當前狀態變為 ON ,如果當前狀態為 ON 那我按下開關時就會把當前狀態變為 OFF 。
在 runed 的 FiniteStateMachine
是這樣寫的:我需要先定義好兩個 type 分別是「當前狀態」跟「事件」,以我們上面的例子來說按下開關就是 toggle
當前狀態就是有 off
跟 on
然後我們就能描述說在哪個狀態下做了什麼事件會切換成什麼狀態了
type ToggleStates = 'off' | 'on';
type ToggleEvents = 'toggle';
const toggleFSM = new FiniteStateMachine<ToggleStates, ToggleEvents>('disabled', {
enabled: {
toggle: 'disabled'
},
disabled: {
toggle: 'enabled'
}
});
在使用上就是使用 current
來拿到「當前狀態」,用 send
來「發送事件」
<div class="mt-12">
toggleFSM {toggleFSM.current}
<button class="btn" onclick={() => toggleFSM.send('toggle')}>Toggle</button>
</div>
當然這個例子有點簡單,來寫一個複雜一點點的例子:咖啡機
當前狀態 | 事件 | 下一狀態 |
---|---|---|
關機 | 按下開機按鈕 | 待機 |
待機 | 按下啟動按鈕 | 研磨 |
研磨 | 研磨完成 | 沖泡 |
沖泡 | 沖泡完成 | 待機 |
待機 | 按下關機按鈕 | 關機 |
研磨 | 按下關機按鈕 | 關機 |
沖泡 | 按下關機按鈕 | 關機 |
簡單地歸納是
按下開機按鈕:
按下啟動按鈕:
研磨完成:
沖泡完成:
按下關機按鈕:
如果狀態是 待機,則轉換為 關機。
如果狀態是 研磨,則轉換為 關機。
如果狀態是 沖泡,則轉換為 關機
那這樣就能寫出他的 FSM 的程式碼:
type CoffeeMachineStates = 'off' | 'idle' | 'grinding' | 'brewing';
type CoffeeMachineEvents = 'powerOn' | 'powerOff' | 'start' | 'grindComplete' | 'brewComplete';
const coffeeMachineFSM = new FiniteStateMachine<CoffeeMachineStates, CoffeeMachineEvents>('off', {
off: {
powerOn: 'idle'
},
idle: {
start: 'grinding',
powerOff: 'off'
},
grinding: {
grindComplete: 'brewing',
powerOff: 'off'
},
brewing: {
brewComplete: 'idle',
powerOff: 'off'
}
});
會發現當我們的狀態愈複雜、事件愈多時使用 FSM 會變得更加容易撰寫。當然 runed 中 FiniteStateMachine
相比於其他 FSM library (xstate 之類的)來說可能還是有那麼一點不足,但如果只是想寫簡單的 FSM 來說還是蠻適合的。
https://github.com/toddLiao469469/30days-for-svelte5/blob/main/src/routes/day13/%2Bpage.svelte