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 模式),但建议按步骤迁移:
- 升级依赖:
npm i svelte@5,迁移工具npx sv migrate svelte-5。 - 先迁状态:把
let x =显性改为$state(),尤其跨闭包/异步的变量。 - 再迁派生:
$: y = f(x)→let y = $derived(f(x))。 - 再迁副作用:
$: sideEffect(x)→$effect(() => sideEffect(x))。 - 最后迁 Props:
export 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>
常见问题 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 的钥匙。
本站提供浏览器本地工具,免注册即可试用 →