Svelte 速查表 - Svelte 框架常用语法大全
面向想少写样板、追求极简运行时的前端开发者。Svelte 在编译期把响应式逻辑直接生成成原生 DOM 更新,因此运行时非常小,也没有虚拟 DOM 心智负担;但状态更新靠赋值、$: 的重算、Store 订阅这些需要建立条件反射。读完能用 let 与 $: 声明式处理响应式,通过事件与派发在组件间通信,用 Store 共享跨组件状态,并接入生命周期与过渡。
典型使用场景
适合从 React/Vue 迁移过来、或在做「组件体积敏感」项目的前端开发者。表里的重点是把 Svelte 的三条主线一次讲清:模板块语法({#each}、{#if}、{#await}、{#key})负责结构,赋值即触发的响应式声明($: 与数组/对象新赋值)负责数据,动作与过渡(use:、transition:、animate:flip)负责交互表现。再往上是用 onMount/onDestroy 管好副作用、用 tick 等待 DOM 更新、用 untrack 切断不该有的依赖追踪;跨层级通信则由 setContext/getContext 承担,模块级脚本 context="module" 负责在组件外复用逻辑。Store 一组用于把状态提到组件之外,$count 自动订阅配合 onDestroy 自动退订,是拆解全局状态时的首选。
基础模板语法 6
{ expression }{@html rawHtmlString}{#if condition}...{:else if}...{:else}...{/if}{#each items as item, index}...{/each}{#await promise}...{:then value}...{:catch error}...{/await}{#key expression}...{/key}响应式声明 5
let count = 0;$: doubled = count * 2;$: { console.log(count); }arr = [...arr, newItem];obj = { ...obj, key: value };组件与 Props 7
export let name;export let name = "default";$$props$$restProps<slot /><slot name="header" /><slot let:item={item} />事件与绑定 7
<button on:click={handler}><form on:submit|preventDefault={handler}>import { createEventDispatcher } from "svelte"; const dispatch = createEventDispatcher(); dispatch("event", data);<input bind:value={name}><input bind:this={inputEl}><input type="checkbox" bind:group={selected}><input type="radio" bind:group={choice}>生命周期 6
import { onMount } from "svelte"; onMount(() => { ... });import { beforeUpdate } from "svelte"; beforeUpdate(() => { ... });import { afterUpdate } from "svelte"; afterUpdate(() => { ... });import { onDestroy } from "svelte"; onDestroy(() => { ... });import { tick } from "svelte"; await tick();import { untrack } from "svelte"; untrack(() => { ... });Store 7
import { writable } from "svelte/store"; export const count = writable(0);import { readable } from "svelte/store"; export const time = readable(new Date(), (set) => { ... });import { derived } from "svelte/store"; export const doubled = derived(count, ($c) => $c * 2);$countcount.set(10);count.update(n => n + 1);import { get } from "svelte/store"; const val = get(count);动作与过渡 7
use:action={{ param }}transition:fadetransition:slidetransition:scalein:fly={{ x: 200, duration: 500 }}out:fly={{ x: -200 }}animate:flip高级特性 5
import { setContext, getContext } from "svelte"; setContext("key", value); const val = getContext("key");import { hasContext } from "svelte"; hasContext("key");<script context="module">...</script>export const greet = (name) => `Hello ${name}`;svelte:options参数矩阵
| 参数 | 作用 | 示例 |
|---|---|---|
{ expression } | 模板中插入 JavaScript 表达式 | <h1>{user.name}</h1> |
{@html raw} | 渲染原始 HTML 字符串,存在 XSS 风险 | {@html post.bodyHtml} |
{#if}...{:else if}...{:else} | 条件渲染分支 | {#if loading}<Spinner />{:else}<List />{/if} |
{#each items as item, i} | 遍历数组渲染,第二项为下标 | {#each todos as todo, i}{i}. {todo.text}{/each} |
{#await promise} | 按 pending/then/catch 三态渲染异步结果 | {#await loadUser()}<Spinner />{:then u}<Profile {u} />{/await} |
{#key expression} | 表达式变化时销毁并重建内部内容 | {#key activeTab}<Panel {tab} />{/key} |
$: doubled = count * 2 | 响应式声明,依赖变化时自动重算 | $: total = price * qty |
arr = [...arr, x] | 数组更新必须赋新引用才触发更新 | items = [...items, newItem] |
obj = { ...obj, k: v } | 对象更新同样要整体赋新引用 | state = { ...state, open: true } |
export let name | 声明组件接收的 prop | export let name; |
$$restProps | 读取未显式声明的其余 props | <input {...$$restProps} /> |
<slot name="header" /> | 具名插槽,供父组件填充指定区域 | <slot name="header" /> |
<slot let:item={item} /> | 作用域插槽,把子组件数据回传父组件 | <slot let:item={item}>{item.label}</slot> |
on:click|preventDefault | 绑定 DOM 事件并使用事件修饰符 | <form on:submit|preventDefault={save}> |
createEventDispatcher() | 子组件向父组件派发自定义事件 | dispatch('change', data) |
bind:value / bind:this | 输入值双向绑定 / 绑定 DOM 元素引用 | <input bind:value={name} bind:this={inputEl} /> |
bind:group | 复选框组绑定到数组、单选框组绑定到变量 | <input type="checkbox" bind:group={selected} value={id} /> |
onMount / onDestroy | 挂载后执行请求 / 销毁前清理定时器与订阅 | onDestroy(() => clearInterval(timer)) |
await tick() | 等待 DOM 更新完成后再读取元素状态 | await tick(); el.focus(); |
untrack(() => ...) | 在响应式声明中临时取消依赖追踪 | untrack(() => log(count)) |
writable / readable / derived | 创建可写、只读、派生三种 Store | const doubled = derived(count, (n) => n * 2) |
get(store) | 在非组件上下文同步读取 Store 当前值 | const val = get(count) |
use:action | 自定义动作函数操作 DOM 元素 | use:tooltip |
transition:fade / slide / scale | 元素出现与消失时播放过渡动画 | transition:slide |
in:fly={{ x: 200 }} | 进入动画(飞入),out: 对应离开动画 | in:fly={{ x: 200, duration: 500 }} |
animate:flip | 列表元素位置变化时补间动画(FLIP) | animate:flip={{ duration: 200 }} |
setContext / getContext | 跨层级组件通信,hasContext 判断是否存在 | setContext('theme', 'dark') |
svelte:options | 编译器选项,如 immutable 关闭深度代理 | <svelte:options immutable={true} /> |
context='module' | 模块级脚本,每个组件实例只执行一次 | <script context="module"> |
易错点与避坑指南
现象push 了数组元素、设置了对象属性,界面却不更新
原因Svelte 的响应式基于赋值追踪,push 与 obj.x = v 这类原地修改不会触发重新赋值
处置改用新引用写法:arr = [...arr, newItem]、obj = { ...obj, key: value }
现象异步数据回来后 DOM 还没更新就去读 input.value,结果拿到旧值
原因赋值只排入了更新队列,DOM 渲染在下一个微任务才发生
处置在赋值后 await tick(),等 DOM 更新完成再读取元素状态
现象组件销毁后仍在打接口,控制台刷请求或出现 setState 警告
原因在 onMount 里发起的请求没有随组件销毁取消
处置把 AbortController 或取消句柄存在变量里,在 onDestroy 里取消请求并清理定时器
现象响应式声明里只想读一次值,却被无关变量变化触发重算
原因$: 块会自动收集块内所有读取的依赖,包含非响应式来源的读取
处置把纯读取放进 untrack(() => ...),让它不参与依赖追踪
现象切换列表项状态后,动画错乱或复用了上一次的 DOM
原因列表里缺少 key 语义,Svelte 按位置复用元素
处置用 {#key expression} 包住需要重建的内容,或调整 {#each} 的来源结构
现象{@html} 渲染用户提交内容后出现脚本被执行
原因{@html} 直接把字符串当 HTML 解析输出,不做转义
处置对用户输入先做净化,或改用普通 { expression } 由 Svelte 自动转义
现象在组件外部或事件回调里读 Store 拿到 undefined
原因$store 自动订阅语法只在组件顶层可用,脱离组件后没有订阅者
处置在非组件上下文用 get(store) 同步取值,或改用 store.subscribe() 手动订阅
排障路径
1确认某个状态是否真的触发了重新渲染
$: console.log(count);在响应式语句块里打印依赖变量,控制台出现新输出说明赋值已被追踪到。
2确认数组/对象更新是否走了新引用
arr = [...arr, newItem];把原地 push 改成整体赋值;若界面随即更新,问题就出在原来那行。
3在 DOM 更新后立即聚焦或测量元素
await tick();tick() 之后的 DOM 已是最新状态,适合 focus()、getBoundingClientRect() 等操作。
4把不该被追踪的读取排除出响应式声明
untrack(() => render(count));日志里若看到 render 因无关变量被重复调用,就把该读取包进 untrack。
5强制重建一段内容而不是复用
{#key activeId}<Panel data={activeId} />{/key}内容切到别的数据源后仍显示旧内容时,用 {#key} 显式声明重建边界。
6在组件之外读取 Store 的当前值
const val = get(count);get() 同步读出当前值,适用于事件回调、定时器、模块顶层等无组件上下文处。
7给子组件派发事件并在父组件接收
dispatch('change', data);父组件用 on:change={handler} 接收,detail 里就是 dispatch 传出的 data。
命令示例
each 带索引,渲染带序号的列表
{#each items as item, index}
<li>{index + 1}. {item}</li>
{/each}index 是 {#each} 的第二个绑定,可以直接参与展示。列表项需要稳定的 key 时用 (item.id) 形式;用对象本身做 key 在数据重新创建时会整块重建 DOM,性能反而更差。
await 块覆盖 loading / 成功 / 失败三态
{#await promise}
<p>加载中…</p>
{:then user}
<p>{user.name}</p>
{:catch error}
<p>{error.message}</p>
{/await}只写 {#await promise}...{/await} 而不写 :then 与 :catch,失败分支不会有任何反馈——请求失败时界面看起来仍在加载。三个分支都写出来,是避免「静默失败」最省事的做法。
Store:非组件上下文里用 get 而不是带dollar前缀的写法
export const count = writable(0);
count.update((n) => n + 1);
// 组件外读值:
import { get } from "svelte/store";
const val = get(count);$count 是编译期语法糖,只能在组件或 .svelte 文件里用;模块顶层的事件处理器、工具函数里必须用 get()。写成 $count 在这些位置会直接编译失败,这类报错往往指向看不出关联的远端文件。
onMount 拉数据,onDestroy 收尾
import { onMount, onDestroy } from "svelte";
let timer;
onMount(() => {
timer = setInterval(refresh, 5000);
});
onDestroy(() => {
clearInterval(timer);
});组件每次挂载都会跑一次 onMount,条件渲染把它卸载时定时器、订阅、WebSocket 都还在跑。清理逻辑必须成对写在 onDestroy 里,否则组件反复进出页面会叠加出多个定时器。
derived 从已有 Store 派生,不重复持有状态
export const doubled = derived(count, (value) => value * 2);常见错误是把派生值再抄一份到组件的局部变量里,于是出现两个真相来源。derived 让它成为 Store,依赖变化自动重算,任何地方订阅到的都是同一个值。
常见坑与注意事项
- {@html rawHtmlString} 直接注入原始 HTML,任何来自用户或接口的内容都等于开放 XSS;除非确知内容可信,否则用普通插值。
- 给子组件的 export let prop 赋值会静默改掉父组件的状态,props 是单向的数据来源,跨层改状态要靠事件或 bind。
- 响应式声明 $: 只追踪其直接出现在语句里的变量;把值藏在对象或经函数参数传入就建立不了依赖,改动后 UI 不更新。
- onMount 里请求数据但不处理组件已销毁的情况,响应回来后写状态会报错或泄漏;在返回的清理函数里加标志位。
- {#key} 会在表达式每次变化时销毁并重建内部全部内容,放在 #each 外面或用在会频繁变化的表达式上会造成整块 DOM 反复重建。
- Svelte 的 html 与 await 指令之外的 SSR 场景下,依赖浏览器全局对象的代码会在服务端崩掉;渲染前先做类型判断,或只在组件挂载后再访问。
提示
- Svelte 是编译型框架,没有运行时,生成的代码极小
- 响应式基于赋值触发,数组和对象需要用新赋值触发更新
- $: reactive 声明可以跟踪任何依赖的变化
- Store 在组件外管理状态,$store 语法自动订阅和取消订阅
- Svelte 5 引入了 Runes($state/$derived/$effect)新响应式系统
常见问题
push 了一条数据,界面为什么不更新?
Svelte 的响应式基于赋值触发,数组和对象必须换成新引用才会更新。用 `arr = [...arr, newItem]` 追加元素、`obj = { ...obj, key: value }` 更新字段,而不是 `arr.push(x)` 或 `obj.k = v` 原地修改。Svelte 5 之后还可以用 Runes 里的 `$state` 建立显式响应式容器,同样要求整体赋值新引用。只要把这两处改成新赋值,视图就会跟着更新。
$: 响应式声明和普通的计算变量有什么区别?
`$: doubled = count * 2;` 是响应式声明,它会分析右边引用了哪些变量,count 变化时自动重算,`$: { console.log(count); }` 这样的语句块同理。普通计算只在声明时求值一次,依赖变了不会重跑。声明式必须写在组件顶层的 `<script>` 里,顺序按依赖拓扑排列;调试时容易遇到的问题是响应式块里新引入的变量本身又形成循环依赖。遇到这种拿不准的依赖追踪,可以在块里用 `untrack(() => { ... })` 临时取消追踪。
组件之间共享状态,什么时候该用 Store?
跨组件共享或需要放在组件树之外管理时用 Store:`import { writable } from "svelte/store"` 创建,`$count` 在组件中自动订阅、组件销毁时自动退订,写入用 `count.set(10)` 或基于当前值 `count.update(n => n + 1)`。只读状态用 `readable`,由别的 Store 计算出来的用 `derived(count, ($c) => $c * 2)`。在组件外的普通函数里取值要用 `get(count)`,因为 `$` 语法只在组件内有效。
父组件要给子组件传内容或数据,slot 和 props 怎么分工?
传结构化的值用 props:`export let name` 声明接收,`export let name = "default"` 给默认值,`$$props` 拿到全部传入项,`$$restProps` 拿未显式声明的剩余部分。传一段模板内容用 `<slot />`,需要多处插槽时用 `<slot name="header" />` 具名插槽。子组件反过来要把数据交回父组件,用作用域插槽 `<slot let:item={item} />`。
组件里什么时候该用生命周期钩子而不是普通函数?
涉及 DOM 或浏览器资源的操作必须放进钩子里。`onMount(() => { ... })` 在挂载后执行,适合发 API 请求;`onDestroy(() => { ... })` 在销毁前执行,用来清定时器、退订,避免内存泄漏;`beforeUpdate` 和 `afterUpdate` 分别在 DOM 更新前后触发。`await tick()` 用来等 DOM 更新完成,典型场景是更新后立刻读取 input 的尺寸。如果只是纯计算,用 `$:` 声明就够了,不必写进钩子。
由 巧匠 维护
公开更新于 2026年7月21日,内容持续校对官方文档。