Svelte Store 狀態管理
tags: Svelte
category: Front-End
description: Svelte Store 狀態管理
created_at: 2023/04/16 22:00:00

前言
終於繼續更新了,上禮拜期中考週
進入到進階篇,第一篇先來寫 Store,也就是狀態管理的部分,狀態管理的概念可以參考我在 Vue 3 的文章,核心概念都是一樣的。
簡單來說就是當你組件開始多了,要傳遞資料的時候可能會需要傳遞多個組件,而中間組件可能根本不需要這些東西,如果有個地方專門存放這些狀態,要的時候直接拿,就會方便許多。
基本使用
在 Svelte 當中定義 Store 非常簡單(相比於其他框架)
不過在建立 Store 之前,先做一個簡單的 Counter:
/src/routes/+page.svelte
<script lang="ts">
let count = 0;
function increment() {
count += 1;
}
function decrement() {
count -= 1;
}
function reset() {
count = 0;
}
</script>
<div>
<h1>Counter</h1>
<p>Count: {count}</p>
<button on:click={increment}>+</button>
<button on:click={decrement}>-</button>
<button on:click={reset}>Reset</button>
</div>
假設這時我希望建立一個 Store 去管理 count 的值,非常簡單
建立檔案: /src/stores/count.ts
import { writable } from 'svelte/store';
export const count = writable(0);
就這麼簡單,你沒看錯
使用他的方式比較像 Angular,是使用訂閱(subscribe)的方式(應該沒改吧,我好幾年沒碰了(如果他還是 )based on rxjs 的話
這時先建立一個新的組件,取代掉呈現的部分:
/src/components/ShowCount.svelte
<script lang="ts">
import { count } from '../stores/count';
let value = 0;
count.subscribe((val) => {
console.log(val);
value = val;
});
</script>
<div>
Count: {value}
</div>
console.log 是給你檢查用的,會看到 console 印出 0。
這時要把 +page.svelte 的內容也換掉:
<script lang="ts">
import ShowCount from '../components/ShowCount.svelte';
import { count } from '../stores/count';
function increment() {
count.update((n) => n + 1);
}
function decrement() {
count.update((n) => n - 1);
}
function reset() {
count.set(0);
}
</script>
<div>
<h1>Counter</h1>
<ShowCount />
<button on:click={increment}>+</button>
<button on:click={decrement}>-</button>
<button on:click={reset}>Reset</button>
</div>
這時你會發現一切運作正常,然後當 count 的值改變的時候,console.log 也會一直觸發。
這個簡單的範例就剛好展示出了他提供的三個方法:
set: 直接設定值update: 基於上一個值來更新subscribe: 訂閱他,當改變會執行
我要退訂
實際上訂閱是需要取消訂閱(unsubscribe)(退訂)的,如果你沒退訂久了可能會發生災難,所以還是養成好習慣。
unsubscribe 的取得方式很簡單,subscribe 函數的回傳值就是 unsubscribe
這時先弄一個很髒的東西來看看沒退訂會怎樣:
+page.svelte
<script lang="ts">
// 略
let value = 0;
count.subscribe((val) => {
value = val;
});
</script>
<div>
<h1>Counter</h1>
<!-- 偷偷交換順序,比較好操作 -->
<button on:click={increment}>+</button>
<button on:click={decrement}>-</button>
<button on:click={reset}>Reset</button>
{#if value % 2 == 0}
<ShowCount />
{/if}
</div>
這時你去多戳幾次增減,會發現你的 console.log 觸發越來越多次,這不是我們要的,所以我們要退訂,像是下面這樣:
ShowCount.svelte
const unsubscribe = count.subscribe((val) => {
console.log(val);
value = val;
});
onDestroy(unsubscribe);
這時在回去戳就正常了。(不會重複 console.log)
自動訂閱 & 退訂
上面訂閱&退訂,多了之後會變得很 routine 很重複,所以有這個 $ 語法糖,跟前面提到的響應式資料不一樣。
可以先像下面這樣確認他有幫我們退訂:
<script lang="ts">
import { count } from '../stores/count';
let value = 0;
$: {
console.log($count);
value = $count;
}
</script>
<div>
Count: {value}
</div>
玩夠了之後再把他簡寫:
<script lang="ts">
import { count } from '../stores/count';
</script>
<div>
Count: {$count}
</div>
超精簡
然後這時 +page.svelte 也可以改寫
<script lang="ts">
import ShowCount from '../components/ShowCount.svelte';
import { count } from '../stores/count';
function increment() {
$count += 1;
}
function decrement() {
$count -= 1;
}
function reset() {
$count = 0;
}
</script>
<div>
<h1>Counter</h1>
<button on:click={increment}>+</button>
<button on:click={decrement}>-</button>
<button on:click={reset}>Reset</button>
{#if $count % 2 == 0}
<ShowCount />
{/if}
</div>
$ 不只可以自動訂閱 & 退訂,還可以直接對他做修改。
這樣改會不會有性能問題?
Svelte 是一個編譯器,直接從他產出的 code 來看,我私心覺得用 $ 可能效能更好?(撇開退訂)
來觀察一下使用 update、set 所產出的如下:
function increment() {
count.update(n => n + 1);
}
function decrement() {
count.update(n => n - 1);
}
function reset() {
count.set(0);
}
而 update 實際上底層也是去呼叫 set;而使用 $ 編譯出來的是:
function increment() {
set_store_value(count, $count += 1, $count);
}
function decrement() {
set_store_value(count, $count -= 1, $count);
}
function reset() {
set_store_value(count, $count = 0, $count);
}
而 set_store_value 函數非常簡單:
function set_store_value(store, ret, value) {
store.set(value);
return ret;
}
也是直接去呼叫 set,但少了像 update 還要先把函數執行完才知道結果的這個步驟。
(也許我不是很理解 svelte,如果有錯可以跟我說XD)
唯讀的 Store
有些時候可能想要定義一個狀態,他是唯讀的,只能給其他地方用但不能改。(只能透過自己改)
所以我這邊想了一個使用情境,假設你在前端做遊戲,你可能需要一個每 1 fps 去增加的 frame,在遊戲過程中可以根據這個 frame 去做一些處理,這時你就不會希望外部可以直接修改這個 frame 值。
先寫下下面的 code,待會再來解釋
/src/stores/frame.ts
import { readable } from 'svelte/store';
const fps = 30;
let value = 0;
export const frame = readable(0, function start(set) {
console.log('start');
const interval = setInterval(() => {
set(++value);
}, 1000 / fps);
return function stop() {
console.log('stop');
value = 0;
set(value);
clearInterval(interval);
};
});
改成使用 readable 宣告,就會只剩下 subscribe 可以使用,外部沒有辦法再去修改他的值,就算你使用前面的語法糖也無法。
而第二個參數帶入一個函數,當有人去訂閱的時候會觸發,可以在 start 函數內回傳一個函數,被回傳的函數(stop)會觸發在最後一個退訂的時候。
所以上面這段就是當有第一個訂閱來的時候會去開始計時器去增加 frame,而當所有地方都退訂,沒有人訂閱的時候就把 frame 歸零。
要測試的話可以把 +page.svelte 變成
<script lang="ts">
import ShowFrame from '../components/ShowFrame.svelte';
let loop = 1;
</script>
<div>
<h1>Game</h1>
<button on:click={() => loop++}>Add</button>
<button on:click={() => loop--}>Delete</button>
{#each { length: loop } as _}
<ShowFrame />
{/each}
</div>
然後建立一個: /src/components/ShowFrame.svelte
<script lang="ts">
import { frame } from '../stores/frame';
</script>
<div>
Frame: {$frame}
</div>
這時你去戳 Add,下方會多渲染一個訂閱 frame 的組件
點擊 Delete 會退訂一個訂閱 frame 的組件
可以觀察 console 的變化,應該很好理解。
衍生自其他 Store
一個 store 也能使用其他 store 的值來做衍生,有點像 Vue 3 的 computed 或 Svelte 的響應式資料的 $。
假設我要一個 time 他是從 frame 去計算的:
/src/stores/time.ts
import { derived } from 'svelte/store';
import { fps, frame } from './frame';
export const time = derived(frame, ($frame) => {
return ($frame / fps).toFixed(2);
});
因為用到了 fps,所以記得把 frame.ts 的 fps 導出。
然後可以建立一個 ShowTime 去使用他
<script lang="ts">
import { time } from '../stores/time';
</script>
<div>
Time: {$time}
</div>
然後在 +page.svelte 就可以使用 ShowTime
<script lang="ts">
import ShowTime from '../components/ShowTime.svelte';
// 略...
let showTime = true;
</script>
<!-- 略... -->
<button on:click={() => (showTime = !showTime)}>Toggle time</button>
<!-- 略... -->
{#if showTime}
<ShowTime />
{/if}
然後也可以戳一戳去觀察他 start 和 stop 的情形。
衍生自多個 Store
import { derived, writable } from 'svelte/store';
export const a = writable(0);
export const b = writable(0);
export const ab = derived([a, b], ([$a, $b]) => {
return $a + $b;
});
可以像這樣使用他:
<script lang="ts">
import { a, b, ab } from '../stores/ab';
</script>
<div>
<h1>A + B</h1>
<div>A: <input type="number" bind:value={$a} /></div>
<div>B: <input type="number" bind:value={$b} /></div>
<div>A + B: {$ab}</div>
</div>
自訂 Store
上面宣告 writable 的時候會暴露出三個函數,假設你不希望外部直接使用,可以把它包裝起來。
import { writable } from 'svelte/store';
export function createCount() {
const { subscribe, set, update } = writable(0);
return {
subscribe,
increment: () => update((n) => n + 1),
decrement: () => update((n) => n - 1),
reset: () => set(0)
};
}
然後可以像這樣使用他
<script lang="ts">
import { createCount } from '../stores/counter';
const count = createCount();
</script>
<div>
<h1>Counter</h1>
<button on:click={() => count.increment()}>+</button>
<button on:click={() => count.decrement()}>-</button>
<button on:click={() => count.reset()}>Reset</button>
<span>{$count}</span>
</div>
總結
這一篇介紹了 Store 的使用方式,真的相比於其他框架簡單太多了XD
用了就回不去了