正式介紹 rune 語法前,我覺得可以先來討論為什麼需要 rune 或者更深層的問題是為什麼需要狀態或者該說為什麼需要前端框架。
相信大部分的人使用前端框架都是為了解決一個常見但很頭痛的問題「畫面與資料的溝通」,這個問題的困難點在於畫面怎麼知道要更新以及要更新什麼。
在 jQuery 時期大部分情況下都是更新資料後手動的操作 DOM ,也就是開發者顯式的告訴程式碼我要「如何」渲染,就會類似下面這段程式碼:
<body>
<p id="count">Count: 0</p>
<button id="incrementBtn">Change color</button>
</body>
<script>
let count = 0;
let $incrementBtn= $('#incrementBtn');
let $count = $('#count')
const handleIncrementBtnClick = ()=>{
count += 1;
$count.text('Count: ' + count);
}
$incrementBtn.click(handleIncrementBtnClick);
</script>
那現代的前端框架是怎麼解決這件事情呢?沒錯就是「狀態」,以 Svelte 來說就是狀態更新後觸發畫面更新,在 Svelte 5 中就是使用 rune 來達成這個功能, 而 rune 就是 Svetle 5 中全新的語法,本質上就是利用特別符號來讓編譯器知道這邊要實現一些特殊功能,像是 $state
、$derived
、 $props
以及 $effect
。
<script>
let count = $state(0)
let handleIncrementBtnClick = () =>{
count += 1;
}
</script>
<main>
<p>Count: {count}</p>
<button onclick={handleIncrementBtnClick}>Increment</button>
</main>
除了 UI 的寫法以外更重要的差異是,在 jQuery 的例子裡我必須要手動在 handleIncrementClick
中執行$count.text('Count: ' + count)
,但在 Svelte 中我就只需要 <p>Count: {count}</p>
,然後只要 count
是 state 那它更新時畫面自然而然會更新,我不必在 handleIncrementClick
裡特別的說明我要更新哪一個元素。
從上面的例子就可以看到這次要介紹的第一個 rune :$state
,也就是宣告 state 的 rune,那寫習慣 React 的讀者可能會想那我怎麼 update 呢?沒錯就直接 mutate 它就好
<script>
let count = $state(0)
const increment = () => {
count += 1;
};
const decrement = () => {
count -= 1;
};
</script>
<h1>
<span>
Count: {count}
</span>
</h1>
<button onclick={increment}>Increment</button>
<button onclick={decrement}>Decrement</button>
<button
onclick={() => {
count = 0;
}}
>
Reset
</button>
object
和 array
的 state 也是這麼簡單,可以看到我們在更新 object state 時就算直接變更某個 property 也能保有 reactive
<script>
let objectState: Record<string, number> = $state({ a: 1, b: 2, c: 3 });
const objectItemIncrement = (key: string) => () => {
objectState[key] += 1;
};
let arrayState = $state([1, 2, 3]);
</script>
<div class="container">
{#each Object.entries(objectState) as [key, value]}
<p class="content">
{key}: {value}
</p>
{/each}
</div>
<button onclick={objectItemIncrement('a')}>a + 1</button>
<button onclick={objectItemIncrement('b')}>b + 1</button>
<button onclick={objectItemIncrement('c')}>c + 1</button>
<button
onclick={() => {
objectState.d = 4;
}}>add d</button
>
<button
onclick={() => {
objectState = { a: 1, b: 2, c: 3 };
}}
>
Reset
</button>
在 array 也是一樣的道理,我可以直接更新特定的 item 也可以直接 push
。
<div>
<span class="content">
{arrayState.join(', ')}
</span>
</div>
<button
onclick={() => {
arrayState.push(arrayState.length + 1);
}}
>
Push new item
</button>
<button
onclick={() => {
arrayState[0] += 1;
}}
>
Increment first item
</button>
<button
onclick={() => {
arrayState = [1, 2, 3];
}}
>
Reset
</button>
雖然不必像 React 的 useState
需要在 setState
進行 immutable update 是比較簡潔沒錯,但我覺得這沒有誰比較好的問題,immutable update 雖然麻煩但可以防止狀態被意外的更新(特別是在 object
、array
type)以及我覺比較好搜尋到程式碼,像是 React 我可以只搜尋 setState
就大概能知道哪邊有更新到 state ,但 $state
我只能搜尋變數名稱除非我清楚知道我要搜尋 varibaleName.property =
但大多數情況我可能還得先搜尋一次 varibaleName
之後再人工判斷是不是更新或讀值。
以及在某些情況下我覺得 immutable update 的可讀性其實是大於 mutable update 的,因為在一次更新多個 property時我可以整理好整份要更新的 state 在一次進行更新,那如果是 mutable update 可能會傾向分成多次的 mutable update 去達成。
Svelte 也有提供一個 rune 讓我們的 state 必須使用 immutable update,就留待以後再介紹吧。
顧名思義它就是要讓我們計算衍生 state 所使用的,那何為衍生的 state 呢?基本上就是我有一個 state 他是「依賴另外一個 state 更新,而跟著一起更新的」
稍微拓展一下上面的 count 的例子
<script>
let count = $state(0);
let count2 = $state(0);
let double = $derived(count * 2);
let total = $derived(count + count2);
const increment = () => {
count += 1;
};
const decrement = () => {
count -= 1;
};
</script>
<p class="content">
Count: {count}
Double: {double}
</p>
<button onclick={increment}>Increment</button>
<button onclick={decrement}>Decrement</button>
<button
onclick={() => {
count = 0;
}}
>
Reset
</button>
<p class="content">
Count2: {count2}
</p>
<button
onclick={() => {
count2 += 1;
}}
>
Increment count2
</button>
<button
onclick={() => {
count2 -= 1;
}}
>
Decrement count2
</button>
<button
onclick={() => {
count2 = 0;
}}
>
Reset count2
</button>
<p class="content">
Total: {total}
</p>
首先多宣告了一個 state count2
以及使用了 $derived
來產生 double
以及 total
let double = $derived(count * 2);
let total = $derived(count + count2);
$derived
的功能就是 double
的值是 count * 2
且每次 count
更新 double
都會重新計算出新的值,而 total
則是 count
或 count2
任一者更新後都會重新計算出 total
。
這樣我們就很能很簡單的去衍生我們的狀態了,但或許有人會想問那像在 React $derived
會是什麼呢?其實就是一般的 const double = state * 2
的變數宣告,那為什麼 React 這樣子就能夠 reactive 而 Svelte 需要使用 $derived
這是因為 React 本質上每次 re-render 都是重新執行一次 function ,所以這個 double
會被重新賦值,如果剛好 state
有變就會算出新的 double
。
所以這兩者的差異會是在 「Svelte $derived
是在依賴的狀態有變動時才會重新計算而 React 每次都會重新賦值」 ,所以也可以延伸到為什麼 React 會需要 useCallback
及 useMemo
,但總的來說, Svelte 5 是有做到 fine-grained reactivity 這件事情的,至於原理我們就留待以後再說吧 XD
當然 rune 遠遠不只這些,但因為今天光是介紹兩個最基本的 rune 篇幅就已經有點太長了XD,明天我們繼續來探討其他 rune 的功能。
https://github.com/toddLiao469469/30days-for-svelte5/blob/main/src/routes/day04/%2Bpage.svelte