因為 Svelte 還擁有 lifecycle fucntion ,所以我們不必為了模擬生命週期而使用 $effect
,而它們基本上在某些情況中可以說是一樣的東西,所以就只是開發時要想什麼時候要使用 lifecycle 或者 $effect
會比較好寫而已 。
目前 Svelte 有四個 lifecycle function ,分別是 onMount
、 beforeUpdate
、afterUpdate
、onDestroy
,但在 Svelte 5 後基本上 beforeUpdate
、afterUpdate
都可以用 $effect.pre
和 $effect
所代替。
<!-- in Counter.svetle -->
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
let obj = $state({ value: 0 });
let derivedObj = $derived({ value: obj.value * 2 });
let p: HTMLParagraphElement | null = $state(null);
$effect.pre(() => {
console.log(
'\x1b[36m%s\x1b[0m',
`[Pre Effect]\n`,
`p.innerText: ${p?.innerText} \n obj.value: ${obj.value}`
);
return () => {
console.log(
'\x1b[36m%s\x1b[0m',
`[Pre Effect Cleanup]\n`,
`p.innerText: ${p?.innerText} \n obj.value: ${obj.value}`
);
};
});
$effect(() => {
console.log(
'\x1b[32m%s\x1b[0m',
`[Effect 1]\n`,
`p.innerText: ${p?.innerText} \n obj.value: ${obj.value}`
);
return () => {
console.log(
'\x1b[32m%s\x1b[0m',
`[Effect 1 Cleanup]\n`,
`p.innerText: ${p?.innerText} \n obj.value: ${obj.value}`
);
};
});
onMount(() => {
console.log(
'\x1b[33m%s\x1b[0m',
`[onMount]\n`,
`p.innerText: ${p?.innerText} \n obj.value: ${obj.value}`
);
});
onDestroy(() => {
console.log(
'\x1b[31m%s\x1b[0m',
`[onDestroy]\n`,
`p.innerText: ${p?.innerText} \n obj.value: ${obj.value}`
);
});
</script>
<button onclick={() => (obj.value += 1)}> Increment obj.value </button>
<button
onclick={() =>
(obj = {
...obj,
value: obj.value + 1
})}
>
Increment obj.value (immutable)</button
>
<p class="content" bind:this={p}>{obj.value} doubled is {derivedObj.value}</p>
初次掛載的輸出是這樣的:
看得出來 onMount
的執行時機是在 DOM 完成掛載後後,而且看這個輸出順序代表onMount 是會在 $effect
第一次執行完才執行嗎?其實不是
onMount(() => {
console.log(
`\x1b[33m%s\x1b[0m`,
`[onMount]\n`,
`p.innerText: ${p?.innerText} \n obj.value: ${obj.value}`
);
});
$effect(() => {
console.log(
'\x1b[32m%s\x1b[0m',
`[Effect 1]\n`,
`p.innerText: ${p?.innerText} \n obj.value: ${obj.value}`
);
return () => {
console.log(
'\x1b[32m%s\x1b[0m',
`[Effect 1 Cleanup]\n`,
`p.innerText: ${p?.innerText} \n obj.value: ${obj.value}`
);
};
});
我們把 onMount
移到 $effect
前就會發現輸出變成
沒錯其實 $effect
的第一次執行跟 onMount
是一樣的東西。可以把 onMount
想成一種只執行一次 $effect
,已知 $effect
就是在 DOM 完成渲染後才會執行,所以這時候純粹就是先執行誰的 effect 的事情而已。
那 onDestroy
也很好理解了,就是我們將 component destroy 時會觸發且也可以把它想像成想成一種只執行一次 $effect
但只有 cleanup 的部分,所以也是先執行 $effect.pre
的 cleanup 後再執行 $effect
的 cleanup 及 onDestroy
看到這裡可能會有疑惑為什麼 onDestroy
或者 $effect
的 cleanup 的 p?.innerText
是有值的?理論上他們執行時機是畫面更新後所以應該會是 undfined
才對,這邊我自己的理解是 destory 是一個特殊的行為,所以他們在 destory 會在 component 真正從 DOM node 被移除之前就先動了。
但 Svelte 提供了一個叫做 tick
的 function ,它會回傳一個 promise 直到任意狀態被改變才會被 resolve 或者沒有狀態的情況下會在下一個 microtask 中 resolve
import { tick } from 'svelte';
onDestroy(() => {
tick().then(() => {
console.log(
'\x1b[31m%s\x1b[0m',
`[onDestroy]\n`,
`p?.innerText: ${p?.innerText} \n obj.value: ${obj.value}`
);
});
});
當我們使用 tick
後他就會直到下一個狀態改變後才會 resolve ,然後才會我們 .then
裡面的 console.log
。
那我們再來統整一次 $effect
、 $effect.pre
、 onMount
和 onDestory
的執行順序
初始狀態: $effect.pre
→ DOM node 掛載完畢 →onMount
/ $effect
狀態更新:$effect.pre
的cleanup → $effect.pre
→ DOM node 更新完畢(如果需要)→ $effect
的 cleanup → $effect
移除 component : $effect.pre
的cleanup → $effect
的 cleanup → onDestroy
這幾天一直在說的 Svelte 會自動追蹤依賴然後依賴更新就會更新狀態或執行 effect,那假設有些情況就是我需要在 A 狀態變更下去看 B 狀態的值但我 B 狀態更新時並不想觸發 effect 呢?這時可以使用 untrack
,這個 function 就是告訴 Svelte 說這個依賴不要被自動追蹤。
以這個 Counter
來說我並不在意第一次渲染時 p
被更新時也去觸發 $effect.pre
但我依然想在 $effect.pre
讀取他的值。
import { untrack } from 'svelte';
$effect.pre(() => {
console.log(
'\x1b[36m%s\x1b[0m',
`[Pre Effect]\n`,
`p.innerText: ${untrack(() => p?.innerText)} \n obj.value: ${obj.value}`
);
可以看到初次掛載後就不會有 p
從 undefined
更新後的 $effect.pre
和它的 cleanup 的 console.log
了。
所以甚至我可以讓一個
$effect
所有的依賴都被untrack
那基本上就跟onMount
沒兩樣了。
$effect
很好用但也很容易被濫用,甚至大多數情況下可以不必使用 $effect
也能達成需求。
就像是 React 的
useEffect
一樣
很明顯的如果這種情況直接使用 onMount
就好,除非真的是想要在 DOM 掛載前就先執行 effect 才會去使用 $effect.pre
像這種情況可以直接改用 $dervied
就好
// ❌ 不要這樣寫
let count = $state(0)
let double = $state(0)
$effect(()=>{
double = count * 2
})
或許可能會想說有些值很複雜不是一個簡單的值就能表示的 $derived
,那這時可以使用 $derived.by
可以傳入一個 function 最後 return 的值就是要改變的值。
// 這兩個是一樣的作用
let double = $derived( count * 2 )
let double = $derived.by(()=> count * 2 )
這邊直接沿用官方文件的範例,這邊會看到兩個 state : spent
及 left
,功能是不管我是改動 spent
還是 left
另外一個都要隨之更新。
<script>
let total = 100;
let spent = $state(0);
let left = $state(total);
$effect(() => {
left = total - spent;
});
$effect(() => {
spent = total - left;
});
</script>
<label>
<input type="range" bind:value={spent} max={total} />
{spent}/{total} spent
</label>
<label>
<input type="range" bind:value={left} max={total} />
{left}/{total} left
</label>
<style>
label {
display: flex;
gap: 0.5em;
}
</style>
bind:value
簡單解釋就是雙向綁定的語法糖,也就是讓 input 的 value 可以影響 state , state 也可以影響 input 的 value,也可以理解為 React 的 controlled component。
不推薦的原因是不管我是選擇去變更哪一個狀態這兩個 $effect
都會被執行,但我們其實預期的是假設我只變更 spent
那應該只有 $effect
第一個執行然後去計算 left
就好。但實際上會變成
更新 spent
→ effect 更新了 left
→ left
更新了所以觸發了 effect 去更新 spent
,不能看出可能會有無限迴圈的問題,也剛好這個例子不會發生。
所以文件給出了一個修改的提案:使用 event 更改狀態
<script>
let total = 100;
let spent = $state(0);
let left = $state(total);
function updateSpent(e) {
spent = +e.target.value;
left = total - spent;
}
function updateLeft(e) {
left = +e.target.value;
spent = total - left;
}
</script>
<label>
<input type="range" value={spent} oninput={updateSpent} max={total} />
{spent}/{total} spent
</label>
<label>
<input type="range" value={left} oninput={updateLeft} max={total} />
{left}/{total} left
</label>
<style>
label {
display: flex;
gap: 0.5em;
}
</style>
今天總算把比較常用 rune 都介紹過一輪了,當然目前 rune 不只這只有這些只是只會在某些特殊場合才會需要,就當未來有用到時在特別提出來介紹吧。
https://github.com/toddLiao469469/30days-for-svelte5/tree/main/src/routes/day06