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
用了就回不去了