Skip to content

Svelte

Svelte 的口号是"编译器,而非运行时框架":组件在构建期被编译成直接操作 DOM 的命令式 JS,浏览器里没有虚拟 DOM、没有庞大运行时——代价是"框架逻辑"编译进产物。Svelte 5 引入 runes(rune = 符文),把响应式从"魔法语法"升级为显式的通用原语。本文基于 Svelte 5(2024-10 发布,当前主线)+ SvelteKit 2;示例为标准写法,可复制进 npm create svelte@latest 模板直接运行。

一、编译时框架:没有虚拟 DOM 的 UI

1.1 一次编译,胜过一次运行时

对比主流框架的更新策略:

框架更新机制运行时成本
React组件函数重跑 → 虚拟 DOM diff → 应用差异每次更新全量 diff(可并发中断)
Vue模板编译期静态分析 + 运行时细粒度响应式组件级更新 + 依赖级缓存
Svelte编译期生成"精确更新指令",值变化直接改对应 DOM 节点无 diff、无虚拟 DOM(旧 Svelte 4 语法时代)
AngularZone.js/信号 触发变更检测组件树检查(可 OnPush 收敛)
svelte
<!-- Counter.svelte(Svelte 5 runes 写法) -->
<script>
  let count = $state(0)          // rune:声明响应式状态
  const inc = () => count += 1
</script>

<button onclick={inc}>点了 {count} 次</button>

编译后这不再是"模板解释器",而是一段带精准 DOM 引用的 JS:count 变化 → 只更新按钮文本节点。

1.2 runes 是什么

Svelte 4 的响应式靠编译器魔法:顶层 let 自动响应、$: 语句标记派生。魔法好写但难迁移、难在普通 .js/.ts 文件里复用。Svelte 5 把能力显式化为一组以 $ 开头的编译期原语——runes

rune作用对应旧语法(legacy)
$state(...)声明响应式状态(深响应)顶层 let(隐式)
$derived(...)由状态派生、惰性缓存的值$: double = count * 2
$effect(...)依赖变化后自动运行的副作用$: console.log(count)
$props()读取组件 propsexport let
$bindable()声明可被父级 bind: 的属性export let + 约定
$inspect(...)调试打印$: console.log
$host()自定义元素模式下取宿主节点

双模式迁移策略:为平滑升级,Svelte 5 中不含任何 rune 的组件默认按 legacy 模式(约等于 Svelte 4 语义)运行;只要组件里出现任一 rune 即整体进入 runes 模式(也可在文件顶层显式开启)。新项目请直接全程 runes,别两边混着写。

1.3 通用响应式:rune 走出组件

runes 可以在任意 .svelte.ts / .svelte.js 模块里用——状态不再绑定组件生命周期,可脱离 UI 独立测试:

ts
// store.svelte.ts:模块级共享状态(编译时把 rune 编译进这个模块)
export const session = $state({ user: null, token: '' })
export const isLoggedIn = $derived(!!session.user)

二、runes 响应式细节

2.1 $state:深响应,基于 Proxy

ts
let profile = $state({ name: 'Ada', tags: [] })

profile.name = 'Grace'              // 触发更新(深层属性也响应)
profile.tags.push('editor')         // 数组方法 OK(Proxy 拦截)
  • 对象与数组默认深响应(Proxy 包裹);
  • $state.raw(...) 可声明浅层、不代理的大对象(性能优化);
  • 想按类实例管理的用 $state(class 实例)(class 会被代理,见官方文档注意事项)。

坑 1:对 $state 对象做解构会丢失响应性——解构出来的是普通值。需要派生若干字段时用 $derived

svelte
<script>
  let user = $state({ name: 'Ada', age: 36 })
  // ❌ const { name } = user   // 之后的 user.name 变化不再响应
  const displayName = $derived(`${user.name} (${user.age})`)  // ✅ 派生表达式内同步读取
</script>

2.2 $derived:惰性、只读

ts
let count = $state(0)
const double = $derived(count * 2)     // 惰性:没人读就不计算
  • 依赖必须同步读取:写在回调/异步里不会被追踪;
  • $derived.by(fn) 形式用于多语句派生。

2.3 $effect:副作用要克制

$effect 在依赖变化后(DOM 更新完)执行,可返回清理函数:

ts
$effect(() => {
  const v = count * 2                 // 同步读取即注册依赖
  document.title = `count=${v}`       // 副作用放这里
  return () => clearTimer()           // 依赖变化/卸载时清理
})

心智锚点与 React 的 useEffect 一致:别用它改别的 $state 去"同步数据"——那会触发新一轮 effect,是循环与闪烁之源。派生一律 $derived。SSR 期间 $effect 不执行(只在浏览器跑)。

2.4 事件与 props:一切皆属性

Svelte 5 移除了 createEventDispatcher——组件通信 = props 传函数,事件就是带 on 前缀的普通属性:

svelte
<!-- Child.svelte -->
<script>
  let { value, onIncrement } = $props()
</script>
<button onclick={onIncrement}>父级的值:{value}</button>
svelte
<!-- Parent.svelte -->
<script>
  import Child from './Child.svelte'
  let n = $state(0)
</script>
<Child value={n} onIncrement={() => n += 1} />
  • 双向:子组件用 $bindable() 声明,父级 bind:value={n}
  • 原生 DOM 事件在 runes 下写 onclick(移除 on: 前缀,也移除 .preventDefault 等修饰符语法,需要手动处理或 onclick={(e) => {...}});
  • 不想透传的属性可用 {...restProps} 展开模式。

三、模板与块语法

3.1 逻辑块

svelte
{#if loading}
  <p>加载中…</p>
{:else if error}
  <p class="err">出错了</p>
{:else}
  <ul>
    {#each items as item (item.id)}   <!-- 括号内为 key -->
      <li>{item.title}</li>
    {/each}
  </ul>
{/if}
  • {#each}key 必须给:否则增删会复用 DOM、输入框状态错位(和 React 的 key 教训同源);
  • {#each obj, i as v, i} 取索引;{#await promise} 处理异步值(见 §3.2)。

3.2 异步块与 key 块

svelte
{#await loadList() then list}
  <ul>{#each list as it}<li>{it.name}</li>{/each}</ul>
{:catch err}
  <p>失败:{err.message}</p>
{/await}

{#key userId}          <!-- 值变化时销毁重建内部,重跑初始化/动画 -->
  <Profile id={userId} />
{/key}

3.3 snippets:可复用模板片断

svelte
<script>
  let users = $state([{ id: 1, name: 'Ada' }, { id: 2, name: 'Grace' }])
</script>

{#snippet row(u)}
  <li>{u.name}(#{u.id})</li>
{/snippet}

<ul>
  {#each users as u}{@render row(u)}{/each}
</ul>
  • 类似函数式模板,可在组件间通过 props 传递(回调式的 slot 替代品);
  • 默认 children 片段:{@render children()}

四、SvelteKit:全栈框架(Svelte 5 + SvelteKit 2)

4.1 文件路由与加载

src/routes/
├── +layout.svelte          # 布局壳(含每个页面的共同 UI)
├── +layout.server.ts       # 布局级服务端加载(登录态等)
├── +page.svelte            # 页面组件
├── +page.server.ts         # 服务端 load:访问 db/密钥,只跑在服务端
├── +page.ts                # 通用 load:前后端都能跑(后端 SSR / 前端导航)
└── blog/[slug]/
    ├── +page.svelte
    └── +page.server.ts     # ({ params }) => ({ post })
ts
// +page.server.ts:load 返回值作为页面组件的 props
import type { PageServerLoad } from './$types'
export const load: PageServerLoad = async ({ params }) => {
  const post = await db.post.findUnique({ where: { slug: params.slug } })
  return { post }                     // 页面里直接 {#if post}{@render ...}{/if}
}
  • Universal vs Server load:需要密钥/直连 DB → +page.server.ts;纯公共数据/想减少服务端往返 → +page.ts
  • 关键点:load 返回的对象若含 Promise,SvelteKit 自动 streaming(外壳先出、数据分块到);
  • 组件内取路由态用 $app/state(SvelteKit 2.12+ 推荐,取代旧 $app/stores 的 store 方式)。

4.2 表单 Actions(无需 JS 也能提交)

ts
// +page.server.ts
export const actions = {
  default: async ({ request }) => {
    const fd = await request.formData()
    const name = String(fd.get('name') ?? '')
    await db.user.create({ data: { name } })
    return { ok: true, name }              // 返回给页面显示
  },
}
svelte
<form method="POST" action="?/">
  <input name="name" required />
  <button>创建</button>
</form>

无 JS 可用表单即交;需要渐进增强时用 use:enhance 变成 SPA 式提交。

4.3 部署与渲染模式

渲染方式手段适用
SSR(默认)首屏服务端渲染后水合大多数动态站
SSG/预渲染export const prerender = true 或项目级配置内容站、文档(配合 adapter-static
CSR(SPA 模式)export const ssr = false纯客户端应用
边缘/Node 服务adapter-node / adapter-vercel / adapter-cloudflare动态接口场景
ts
// svelte.config.js:SSG 需要 adapter-static + prerender 配置
import adapter from '@sveltejs/adapter-static'
export default {
  kit: {
    adapter: adapter({ pages: 'build', assets: 'build', fallback: undefined }),
    prerender: { entries: ['/', '/about'] },
  },
}

五、常见坑速查

  1. legacy 与 runes 混写:同一组件只要出现 rune 就整体进入 runes 模式,旧语法(export let$:on:click)同时混用会行为混乱——迁移时按文件整份处理;
  2. 解构 $state 对象丢响应(§2.1 坑 1):派生用 $derived,别指望解构出的变量联动;
  3. $effect 里写 $state:改完再触发 effect → 循环或闪烁——"数据联动"交给 $derived,effect 只做外部副作用;
  4. $derived 在回调/异步里读依赖:不会注册依赖,值永远不更新——必须同步读取;
  5. {#each} 不给 key:DOM 复用错位、输入框状态串行(§3.1);
  6. 深响应对象整体替换obj = { ...obj, a: 1 } 会把旧的 proxy 整个换掉,依赖它的 $derived 会重算但若有外部引用会指向旧对象——需要时用 $state.snapshot(obj) 取值;
  7. 在 load 里 throw 后页面无提示:SvelteKit 里 throw error(404, '...') 才能正确走 +error.svelte,直接 throw 新 Error 只对顶层兜底可见;
  8. 开发环境"hot 报错"与 $inspect 残留$inspect 记得移除,模板里误留 {} 表达式会直接编译报错(编译器信息通常很具体,跟着改即可)。

状态与参考

  • 状态:已收录(2026-09-02,由前端领域规划清单「Svelte|编译时框架、细粒度响应、SvelteKit 全栈」转正式)。
  • 版本:基于 Svelte 5(2024-10 发布,当前主线,runes 为默认推荐写法;legacy 模式用于平滑迁移)+ SvelteKit 2($app/state 自 2.12+ 提供)。文中示例为标准 runes 写法,可复制进 npm create svelte@latest 模板直接运行;版本细节以 官方 Releases 为准。
  • 参考Svelte 官方文档Svelte 5 迁移指南SvelteKit 文档runes 详解
  • 阅读联动:与 React/Vue 的渲染机制差异对照前端领域概览React/Vue 3;细粒度更新与信号理论可对照 Angular 信号章节;编译期思路见构建工具

下一步

  • [ ] 镜像 Svelte 官方文档分篇(类似 Vue 3 栏目),把 runes、snippets、SvelteKit load/actions 展开为独立子页
  • [ ] 跑通一个 SvelteKit「表单 actions + 流式 load + adapter-static 预渲染」三模式混合示例
  • [ ] 对照 React Compiler 与 Vue 响应式,写"编译器优化 vs 运行时信号"机制对比

写作规范请参阅领域概览

基于 VitePress 构建 · 内容以知识共享方式沉淀