Svelte 5 Runes 实战:从 $state、$derived、$effect 到 Svelte 4 迁移

前端工程

Svelte 5 为什么值得关注

Svelte 5 用 Runes(符文) 重写了响应式系统,从「编译期依赖追踪」升级为「显式、细粒度、跨文件」的响应式原语。它解决了 Svelte 4 时代 let 变量在闭包/异步/跨模块场景下的追踪失效问题,同时带来更小的运行时和更高的性能。

维度 Svelte 4 Svelte 5 (Runes)
状态声明 let count = 0 let count = $state(0)
派生值 $: doubled = count * 2 let doubled = $derived(count * 2)
副作用 $: console.log(count) $effect(() => console.log(count))
跨模块共享 需 stores 可在 .svelte.js 直接用 Runes
组件 Props export let let { x } = $props()

核心 Runes 一览

$state:声明响应式状态

<script>
  let count = $state(0);
  let user = $state({ name: "Ada", age: 36 });

  function inc() {
    count++;                 // 自动触发更新
    user.age++;              // 深层属性同样响应式(代理实现)
  }
</script>

<button onclick={inc}>{count} - {user.age}</button>

注意:$state 对对象和数组使用 深度代理(deep proxy),所以 user.age++ 也能精准追踪,不再需要 Svelte 4 的「整体替换」写法。

$derived:声明派生值

<script>
  let price = $state(100);
  let qty = $state(2);
  let total = $derived(price * qty);     // 仅在依赖变化时重算
  let label = $derived.by(() => {
    // 复杂逻辑用 .by 包裹函数
    return price > 0 ? `¥${price * qty}` : "无效价格";
  });
</script>

<p>合计:{total} / {label}</p>

$effect:声明副作用

<script>
  let count = $state(0);
  $effect(() => {
    // 读取 count 即建立依赖,count 变化重新执行
    document.title = `计数:${count}`;
  });
</script>

$effect 只在浏览器运行,适合 DOM 同步、第三方库绑定、订阅清理等。需要清理时用返回函数:

<script>
  let el = $state();
  $effect(() => {
    if (!el) return;
    const obs = new ResizeObserver(() => {/* ... */});
    obs.observe(el);
    return () => obs.disconnect();   // 自动清理
  });
</script>

<div bind:this={el}></div>

Props 与双向绑定

$props 替代 export let

<!-- Child.svelte -->
<script>
  let { title, count = 0, onchange } = $props();
</script>

<h2>{title}: {count}</h2>

$bindable 实现可绑定 Props

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

<input bind:value />

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

<Child bind:value={name} />
<p>父组件:{name}</p>

.svelte.js 模块里复用 Runes

Svelte 5 的重大进步:Runes 不再局限于 .svelte 文件,可在 .svelte.js / .svelte.ts 中声明响应式状态,实现真正的跨组件状态复用。

// counter.svelte.js
export const createCounter = () => {
  let count = $state(0);
  const inc = () => count++;
  const double = $derived(count * 2);
  return { get count() { return count; }, get double() { return double; }, inc };
};
<script>
  import { createCounter } from "./counter.svelte.js";
  const c = createCounter();
</script>

<button onclick={c.inc}>{c.count} / {c.double}</button>

当组件状态涉及复杂配置(如主题色、对比度阈值),可先用 JSON 格式化 工具组织好配置对象,再用 Runes 包装成响应式 store。


生命周期的变化

Svelte 4 Svelte 5 说明
onMount onMount 仍可用 仅挂载时一次
onDestroy $effect 返回清理函数 替代销毁清理
$: 副作用 $effect 响应式副作用统一入口
beforeUpdate/afterUpdate $effect.pre / $effect 更细粒度控制
<script>
  import { onMount } from "svelte";
  let ready = $state(false);
  onMount(() => { ready = true; });   // 挂载逻辑仍可用
</script>

从 Svelte 4 平滑迁移

Svelte 5 兼容大部分 Svelte 4 语法(legacy 模式),但建议按步骤迁移:

  1. 升级依赖npm i svelte@5,迁移工具 npx sv migrate svelte-5
  2. 先迁状态:把 let x = 显性改为 $state(),尤其跨闭包/异步的变量。
  3. 再迁派生$: y = f(x)let y = $derived(f(x))
  4. 再迁副作用$: sideEffect(x)$effect(() => sideEffect(x))
  5. 最后迁 Propsexport let$props(),需要双向的加 $bindable
<!-- 迁移前 (Svelte 4) -->
<script>
  export let value = 0;
  $: doubled = value * 2;
  $: console.log(doubled);
</script>

<!-- 迁移后 (Svelte 5) -->
<script>
  let { value = 0 } = $props();
  let doubled = $derived(value * 2);
  $effect(() => console.log(doubled));
</script>

涉及颜色、对比度等视觉配置时,可借助 颜色转换颜色对比度 工具验证主题可访问性(WCAG)。


常见问题 FAQ

Q1:Runes 能在普通 .js 文件用吗?

不能。必须在 .svelte.svelte.js.svelte.ts 文件中使用,编译器才能识别。

Q2:$effect 里的非响应式变量变化会触发吗?

不会。$effect 只追踪执行期间被读取的响应式状态。读取普通变量不会建立依赖。

Q3:Svelte 4 代码能直接跑在 Svelte 5 吗?

大部分可以(legacy 兼容)。但混用 Runes 与旧语法时,同一组件内不能混,建议按组件逐步迁移。

Q4:$derived 和 $effect 怎么选?

派生「值」用 $derived;「做事情」(DOM、订阅、日志)用 $effect

Q5:性能提升来自哪里?

细粒度追踪 + 编译期优化,避免了虚拟 DOM 的 diff 开销,状态更新直接命中最小 DOM 节点。


工具推荐

在 Svelte 5 开发中,以下 工具库 能帮上忙:


Svelte 5 的 Runes 不是语法糖,而是响应式心智模型的升级:状态显式、依赖精确、跨文件复用。掌握 $state / $derived / $effect / $props,你就握住了下一代 Svelte 的钥匙。

本站提供浏览器本地工具,免注册即可试用 →

#Svelte#Svelte 5#Runes#前端框架#响应式#前端