跳至主要內容

Svelte 5 Runes:從信號(Signal)出發的全新響應式系統

6 分鐘閱讀 1,000 字

Svelte 5 Runes:從信號(Signal)出發的全新響應式系統

Svelte 5 是一次意義深遠的重寫。儘管外表看起來像個版本號,它實際上代表了 Svelte 響應式模型的根本性變革——從基於編譯時靜態分析的方式,轉向以**信號(Signal)**為核心的 Runes API。

為什麼需要 Runes?

Svelte 4 的響應式系統依賴編譯器識別哪些變數是響應式的。這個方法在簡單場景下優雅,但有個關鍵限制:響應式邏輯無法跨越元件邊界抽取為可複用的模組

// Svelte 4:響應式邏輯只能在元件內使用
// ❌ 這個函式沒有響應式特性
function createCounter() {
  let count = 0; // 普通變數,不是響應式的
  return { count, increment: () => count++ };
}

Runes 讓響應式成為一等公民,可以在任何地方使用。

$state:宣告響應式狀態

<script>
  // Svelte 4
  let count = 0;

  // Svelte 5 with Runes
  let count = $state(0);
</script>

<button onclick={() => count++}>
  點擊次數:{count}
</button>

對物件和陣列,$state 會建立深層響應式代理:

<script>
  let user = $state({
    name: "Alice",
    address: {
      city: "台北"
    }
  });

  // 深層屬性的修改也會觸發更新
  function updateCity(city) {
    user.address.city = city; // 觸發響應式更新
  }

  // 陣列操作同樣有響應式
  let todos = $state([]);
  function addTodo(text) {
    todos.push({ text, done: false }); // push 也是響應式的
  }
</script>

$derived:計算屬性

<script>
  let width = $state(10);
  let height = $state(5);

  // 當 width 或 height 改變時,area 自動重新計算
  let area = $derived(width * height);

  // 複雜計算使用 $derived.by
  let stats = $derived.by(() => {
    const total = todos.length;
    const done = todos.filter(t => t.done).length;
    return { total, done, pending: total - done };
  });
</script>

<p>面積:{area}({width} × {height})</p>
<p>待辦:{stats.pending} / {stats.total}</p>

$effect:副作用

<script>
  let query = $state("");
  let results = $state([]);

  // 類似 React 的 useEffect,但不需要手動宣告依賴
  $effect(() => {
    if (query.length < 2) {
      results = [];
      return;
    }

    // query 改變時自動重新執行
    fetch(`/api/search?q=${query}`)
      .then(r => r.json())
      .then(data => results = data);

    // 清理函式(在下次執行前或元件銷毀時呼叫)
    return () => {
      // 取消進行中的 fetch
    };
  });
</script>

重要:$effect 只在瀏覽器中執行,不在 SSR 時執行。

$props:元件 Props

<script>
  // Svelte 4
  export let name;
  export let count = 0;

  // Svelte 5
  let { name, count = 0, onchange } = $props();
</script>

接受所有 props(類似 Vue 的 v-bind):

<script>
  let { class: className, style, ...restProps } = $props();
</script>

<div class={className} {style} {...restProps}>
  <slot />
</div>

可複用的響應式邏輯(Runes 的殺手級功能)

這是 Runes 最大的突破:響應式邏輯可以抽取到普通的 .svelte.ts 檔案中:

// counter.svelte.ts
export function createCounter(initial = 0) {
  let count = $state(initial);
  let doubled = $derived(count * 2);

  function increment() { count++; }
  function decrement() { count--; }
  function reset() { count = initial; }

  return {
    get count() { return count; },
    get doubled() { return doubled; },
    increment,
    decrement,
    reset
  };
}

// 可以在多個元件中使用
// ComponentA.svelte
const counter = createCounter(10);

// ComponentB.svelte
const counter = createCounter();

更複雜的例子:全域狀態管理(不需要 store):

// stores/cart.svelte.ts
function createCartStore() {
  let items = $state<CartItem[]>([]);

  let total = $derived(
    items.reduce((sum, item) => sum + item.price * item.quantity, 0)
  );

  let itemCount = $derived(
    items.reduce((sum, item) => sum + item.quantity, 0)
  );

  return {
    get items() { return items; },
    get total() { return total; },
    get itemCount() { return itemCount; },

    addItem(product: Product) {
      const existing = items.find(i => i.id === product.id);
      if (existing) {
        existing.quantity++;
      } else {
        items.push({ ...product, quantity: 1 });
      }
    },

    removeItem(id: string) {
      items = items.filter(i => i.id !== id);
    }
  };
}

// 單例模式:整個應用共用同一個購物車
export const cart = createCartStore();

$bindable:雙向綁定 Props

<!-- TextInput.svelte -->
<script>
  let { value = $bindable("") } = $props();
</script>

<input bind:value />

<!-- Parent.svelte -->
<script>
  let text = $state("");
</script>

<TextInput bind:value={text} />
<p>你輸入了:{text}</p>

Svelte 4 vs Svelte 5 對比

概念 Svelte 4 Svelte 5
響應式狀態 let count = 0 let count = $state(0)
計算屬性 $: doubled = count * 2 let doubled = $derived(count * 2)
副作用 $: { ... } $effect(() => { ... })
Props export let name let { name } = $props()
響應式跨元件 需要 store 原生支援
細粒度更新 元件層級 屬性層級

遷移策略

Svelte 5 對 Svelte 4 的語法完全向後相容,可以逐步遷移:

  1. 升級到 Svelte 5,現有元件無需修改即可運行
  2. 新建元件直接使用 Runes
  3. 逐步將複雜邏輯重構為 .svelte.ts 的可複用函式

小結

Svelte 5 的 Runes 是信號(Signal)理念在 Svelte 生態的完整實現,與 Solid.js、Angular Signals、Vue 的 Composition API 共享相似的設計哲學。最重要的突破是響應式邏輯的可攜性——它現在可以跨元件、跨檔案地被複用,這解決了 Svelte 4 最主要的痛點,讓大型應用的狀態管理變得更加優雅。

分享這篇文章