为什么要有 Runes

Svelte 4 的响应式是“编译器魔法”:给变量赋值就自动更新,$: 声明派生值。这套写法新手友好,但在组件变大后会暴露问题——

  • 只有顶层 let 赋值才触发更新,函数内部修改数组/对象需要额外处理;
  • $: 的依赖分析靠静态编译,跨文件、跨函数时容易失效;
  • 无法把响应式逻辑抽到 .js 文件复用。

Runes(符文) 是 Svelte 5 给出的答案:用显式的 $state、$derived、$effect 函数标记响应式,规则统一、可跨文件、可推理。

三大核心 Runes

$state:声明响应式状态

<script>
  let count = $state(0);
  const user = $state({ name: "Ada", tags: ["math"] });

  function inc() {
    count += 1;
    // 深层属性修改同样触发更新(基于 Proxy)
    user.tags.push("logic");
  }
</script>

<button onclick={inc}>{count} · {user.tags.length}</button>

不再需要 svelte/store 的 $store 语法糖,也不再需要 $: 去声明派生。

$derived:声明派生值

<script>
  let price = $state(10);
  let qty = $state(2);

  // 自动追踪依赖,同步计算
  let total = $derived(price * qty);

  // 复杂逻辑用 $derived.by
  let label = $derived.by(() => {
    const t = price * qty;
    return t > 100 ? `大额 ${t}` : `小计 ${t}`;
  });
</script>

<p>{total} — {label}</p>

$derived 是惰性求值的,且不会被重复触发——这一点比 $: 更可控。

$effect:副作用

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

  $effect(() => {
    const controller = new AbortController();
    fetch(`/api/search?q=${query}`, { signal: controller.signal })
      .then((r) => r.json())
      .then((data) => (results = data));

    // 清理函数:下次执行或销毁时调用
    return () => controller.abort();
  });
</script>

<input bind:value={query} />
<ul>{#each results as r}<li>{r.title}</li>{/each}</ul>

注意:$effect 只在客户端运行,服务端渲染时会被跳过。能用 $derived 表达的,就不要写 $effect。

跨文件复用:.svelte.js

Runes 最大的工程价值是响应式逻辑可以放进普通 JS 模块:

// store.svelte.js —— 文件名必须含 .svelte.
function createCart() {
  let items = $state([]);

  return {
    get items() {
      return items;
    },
    get total() {
      return items.reduce((s, i) => s + i.price * i.qty, 0);
    },
    add(item) {
      const found = items.find((i) => i.id === item.id);
      if (found) found.qty += 1;
      else items.push({ ...item, qty: 1 });
    },
    remove(id) {
      items = items.filter((i) => i.id !== id);
    },
  };
}

export const cart = createCart();
<script>
  import { cart } from "./store.svelte.js";
</script>

<button onclick={() => cart.add({ id: 1, price: 9.9 })}>
  加入购物车(共 {cart.total})
</button>

从 Svelte 4 迁移

Svelte 4Svelte 5
let x = 0let x = $state(0)
$: y = x * 2let y = $derived(x * 2)
$: { sideEffect() }$effect(() => sideEffect())
export let proplet { prop } = $props()
createEventDispatcherlet { onEvent } = $props() 回调
on:clickonclick
# 官方迁移工具自动改写大部分写法
npx svelte-migrate svelte-5

性能与体验

Svelte 5 把内部实现换成了细粒度信号(signals),更新不再以组件为单位重跑,而是精确到绑定。官方 benchmark 中,大型列表的更新耗时下降明显,同时包体积依旧显著小于 React + 运行时的组合。

小结

Runes 让 Svelte 从“靠编译器猜意图”变成“开发者显式声明意图”。代价是要多敲几个字符,收益是可预测、可复用、可扩展——对中大型项目而言,这笔交易非常划算。