Svelte Runes Reference (Svelte 5)
本技能覆盖 Svelte 5 的 Runes(符文)系统。Runes 是 Svelte 5 引入的显式响应式语法,取代了 Svelte 4 的隐式 let 声明和 $: 语句。
When to use this skill
当用户需要理解或使用 $state、$derived、$effect、$props、$bindable、$inspect、$host 等符文,或需要将 Svelte 4 代码迁移到 Svelte 5 时使用本技能。
Critical: Runes Overview
Runes 是以 $ 为前缀的符号,类似于函数调用语法:
let message = $state('hello');
关键区别:
- 无需导入 — Runes 是语言内置关键字
- 不是值 — 不能赋值给变量或作为函数参数
- 位置敏感 — 仅在特定位置有效(编译器会报错)
| Rune | 用途 |
|---|---|
$state |
创建响应式状态 |
$derived |
声明派生计算值 |
$effect |
声明副作用 |
$props |
声明组件属性 |
$bindable |
可绑定 prop |
$inspect |
开发调试 |
$host |
自定义元素访问 |
Critical: $state
创建响应式状态,UI 在状态变化时自动更新。
let count = $state(0);
let user = $state({ name: 'Ada', age: 30 });
深层响应式代理
$state 对数组和简单对象自动创建深层 Proxy,属性变更自动触发更新:
let todos = $state([
{ done: false, text: 'task' }
]);
todos[0].done = true; // ✅ 触发更新
todos.push({ done: false }); // ✅ 触发更新
$state.raw(避免深层代理)
适用于大数组和无需深层变更的场景:
let list = $state.raw([]);
// 只能重新赋值,不能 .push()
list = [...list, newItem]; // ✅
list.push(newItem); // ❌ 无效
$state.raw内部仍可包含响应式状态(如原始数组里放代理对象)。
$state.snapshot(静态快照)
获取深层代理的只读快照(用于传给外部 API):
console.log($state.snapshot(proxyObject));
若值有
toJSON()方法,snapshot 会克隆toJSON()的返回值。
$state.eager(即时 UI 更新)
用于 await 表达式中立即更新 UI:
<nav>
<a href="/" aria-current={$state.eager(pathname) === '/' ? 'page' : null}>home</a>
</nav>
类字段中的 $state
class Counter {
count = $state(0); // 公共字段
#value = $state(0); // 私有字段
constructor(start = 0) {
this.value = $state(start); // constructor 中初始化
}
// 箭头函数字段 → this 自动绑定
reset = () => { this.count = 0; };
}
编译器将
$state字段转换为原型上的get/set方法,指向私有字段。这些属性不可枚举。注意this绑定:方法直接传给事件处理器会丢失this,用箭头函数字段或内联() => todo.reset()。
内置响应式类(svelte/reactivity)
普通 Map/Set/Date/URL 不响应 —— 改用响应式版本:
import { SvelteSet, SvelteMap, SvelteDate, SvelteURL } from 'svelte/reactivity';
const tags = new SvelteSet(['a', 'b']);
const cache = new SvelteMap();
tags.add('c'); // ✅ 触发更新
cache.set('a', 1); // ✅ 触发更新
解构陷阱
解构后丢失响应式(与普通 JS 行为一致):
let { name, age } = $state({ name: 'Ada', age: 30 });
name = 'Bob'; // ❌ 不会触发更新,原对象不变
跨模块传递状态
.svelte.js/.svelte.ts 文件中可使用 Runes,但不能直接 export let 重新赋值的 $state:
// ❌ 不可行:另一文件读到的会是 Signal 对象
export let count = $state(0);
// ✅ 方案1:不重新赋值整个对象
export const counter = $state({ count: 0 });
export function increment() { counter.count += 1; }
// ✅ 方案2:模块内私有 + 函数导出
let _count = $state(0);
export function getCount() { return _count; }
export function setCount(n) { _count = n; }
将 state 传入函数 —— getter 模式
JavaScript 是按值传递,$state 同理。若函数需读取最新值,传入 getter:
/** @param {() => number} getA @param {() => number} getB */
function add(getA, getB) {
return () => getA() + getB();
}
let a = $state(1);
let b = $state(2);
const total = add(() => a, () => b);
console.log(total()); // 3
a = 3; b = 4;
console.log(total()); // 7
也可借助 proxy 属性或 get/set 属性实现"实时读取"。文档:"Note that 'functions' is broad — it encompasses properties of proxies and get/set properties."
Critical: $derived
声明派生值——基于已有状态的只读计算值。
let count = $state(0);
let doubled = $derived(count * 2);
表达式必须纯净 —— 内部不能修改
$state(编译器报错)。
$derived.by(复杂派生)
let total = $derived.by(() => {
let sum = 0;
for (const item of items) sum += item.price;
return sum;
});
派生值覆盖(Svelte 5.25+,乐观 UI)
let likes = $derived(post.likes);
async function onclick() {
likes += 1; // 即时乐观更新
try {
await like();
} catch {
likes -= 1; // 回滚
}
}
const派生只读;用let才能重新赋值。文档:"Prior to Svelte 5.25, deriveds were read-only."
理解依赖追踪
$derived 内部同步读取的所有 $state/$derived 都是依赖:
let a = Promise.resolve(1);
let b = 2;
let sum = $derived(await a + b);
// a 和 b 都是依赖(await 之后的同步读取也会追踪)
仅表达式自身的
await后的同步读才算依赖;调用函数内部的await不计入。
用 untrack 排除非依赖值。
Push-pull 响应
派生值只在被读取时重新计算(pull),但状态变化时立即通知所有依赖(push)。若派生返回的引用未变,下游不更新:
let count = $state(0);
let large = $derived(count > 10); // 布尔
// 大文本节点只在 large 变化时重渲染,而非 count
派生内避免
() => ({})、[...].map(...)这类返回新引用的写法 —— 会让下游始终重算。
派生不解构代理
$derived 不会把返回值包成 Proxy —— 修改派生返回对象的属性会影响到底层 $state:
let items = $state([...]);
let selected = $derived(items[0]);
selected.name = 'new'; // ✅ 影响 items[0].name
派生解构
let { first, last } = $derived(user);
// 等价于:
// let first = $derived(user.first);
// let last = $derived(user.last);
Critical: $effect
声明副作用——DOM 操作、第三方库调用、网络请求等。不应用 $effect 同步状态。
$effect(() => {
document.title = `count: ${count}`;
return () => { /* 清理函数 */ };
});
生命周期
- 挂载后首次执行
- 状态变化时 microtask 调度(批量)
- DOM 更新之后执行
- 仅浏览器执行(SSR 自动跳过)
- 可在组件任意位置调用,只要在父 effect 运行期间
依赖追踪
自动追踪 $state/$derived 的同步读取:
$effect(() => {
// color 和 size 是依赖
ctx.fillStyle = color;
ctx.fillRect(0, 0, size, size);
});
异步不追踪
await 之后和 setTimeout 内部的读取不追踪:
$effect(() => {
ctx.fillStyle = color; // ✅ 追踪
setTimeout(() => {
ctx.fillRect(0, 0, size, size); // ❌ size 不追踪
}, 0);
});
条件分支决定依赖
effect 只追踪上次运行时实际读取的状态:
$effect(() => {
if (condition) {
confetti({ colors: [color] });
} else {
confetti();
}
});
// condition=false 时 color 不再是依赖
$effect.pre(DOM 更新前运行)
$effect.pre(() => {
if (!div) return; // 挂载前跳过
messages.length; // 显式依赖追踪
// DOM 更新前执行(如滚动位置计算)
});
$effect.tracking(判断追踪上下文)
console.log($effect.tracking()); // false(组件初始化)
$effect(() => {
console.log($effect.tracking()); // true(effect 内)
});
用于
createSubscriber这类工具:仅在状态被追踪时订阅,事件处理器中不订阅。
$effect.pending()(待定 Promise 数量)
返回当前 boundary(不含子 boundary)中待定的 Promise 数量:
<script>
let a = $state(1);
let b = $state(2);
async function add(a, b) {
await new Promise(f => setTimeout(f, 500));
return a + b;
}
</script>
<button => a++}>a++</button>
<button => b++}>b++</button>
<p>{a} + {b} = {await add(a, b)}</p>
{#if $effect.pending()}
<span>pending: {$effect.pending()}</span>
{/if}
$effect.root()(独立作用域)
创建非追踪作用域,不随组件销毁自动清理:
import { flushSync } from 'svelte';
const destroy = $effect.root(() => {
let count = $state(0);
$effect(() => console.log(count));
return () => { /* cleanup */ };
});
count = 1;
flushSync(); // 立即跑 pending effects
// later...
destroy();
适用:测试工具、组件外创建 effect、模块级 effect。文档:"This rune also allows for the creation of effects outside of the component initialisation phase."
清理函数(Teardown)
$effect(() => {
const interval = setInterval(() => count += 1, 1000);
return () => clearInterval(interval); // 清理
});
teardown 在 (a) effect 重跑前、(b) 组件销毁时执行。
何时不用 $effect
| 需求 | 正确方案 |
|---|---|
| 派生计算值 | $derived |
| 双向同步 | 函数绑定 bind:value={() => v, setter} |
| 无限循环 | untrack 包裹读取 |
| 模板内 await | 直接 await |
文档:"In general,
$effectis best considered something of an escape hatch — useful for things like analytics and direct DOM manipulation — rather than a tool you should use frequently."
Critical: $props
声明组件属性(props):
let { name, age = 18, ...rest } = $props();
类型安全
<script lang="ts">
// 方式 1:内联注解
let { title }: { title: string } = $props();
// 方式 2:接口分离
interface Props { adjective: string; }
let { adjective }: Props = $props();
</script>
JSDoc 风格:
<script>
/** @type {{ adjective: string }} */
let { adjective } = $props();
</script>
原生 DOM 元素的属性类型在
svelte/elements中(如HTMLButtonAttributes)。
prop 默认值
let { adjective = 'happy', count = 0 } = $props();
默认值不是响应式代理 —— 父组件未传时修改其属性不触发更新。文档:"Fallback values are not turned into reactive state proxies."
prop 重命名
let { super: hero = 'default' } = $props();
let { 'class': className, 'for': htmlFor } = $props();
更新 props
Props 在父组件变化时自动更新,但子组件不应直接修改 prop(除 $bindable 外):
// ❌ 不要修改
let { object } = $props();
object.count += 1; // 警告:ownership_invalid_mutation
// ✅ 用 callback props 或 $bindable
子组件可以临时重新赋值 prop(unsaved 状态),父级不受影响。
Rest Props
let { variant = 'primary', size = 'md', ...rest } = $props();
<button class="btn btn-{variant} btn-{size}" {...rest}>
<slot />
</button>
类型:
{ variant?: 'primary' | 'secondary'; [key: string]: any }或HTMLButtonAttributes。
$props.id()(Svelte 5.20+)
生成当前组件实例唯一的 ID,SSR 时 server/client 一致:
<script>
const uid = $props.id();
</script>
<form>
<label for="{uid}-firstname">First Name:</label>
<input id="{uid}-firstname" type="text" />
</form>
适用:
for/aria-labelledby/aria-describedby等需要 ID 配对的场景。
泛型组件
<script lang="ts" generics="T extends { id: number }">
import type { Snippet } from 'svelte';
let {
items,
renderItem
}: {
items: T[];
renderItem: Snippet<[T]>;
} = $props();
</script>
{#each items as item (item.id)}
{@render renderItem(item)}
{/each}
Critical: $bindable
允许子组件修改父组件状态的 prop 类型——双向数据流:
<!-- FancyInput.svelte -->
<script>
let { value = $bindable(), ...props } = $props();
</script>
<input bind:value={value} {...props} />
<!-- 父组件 -->
<script>
import FancyInput from './FancyInput.svelte';
let message = $state('hello');
</script>
<FancyInput bind:value={message} />
<p>{message}</p>
用法
// 必须放在解构的默认值位置
let {
value = $bindable(), // 必填
other = $bindable('hello'), // 带 fallback
} = $props();
父组件两种用法
<!-- 双向 -->
<FancyInput bind:value={message} />
<!-- 单向(父不监听子) -->
<FancyInput value={message} />
文档:"The parent component doesn't have to use
bind:— it can just pass a normal prop. Some parents don't want to listen to what their children have to say."
何时使用
| 场景 | 推荐 |
|---|---|
| 受控表单组件 | $bindable |
| 子→父事件通知 | callback props |
| 跨组件共享 | Context / store |
文档原话:"This isn't something you should do often — overuse can make your data flow unpredictable and your components harder to maintain."
与函数绑定组合
<script>
let { value = $bindable(0), min = 0, max = 100 } = $props();
function clamp(v: number) { return Math.max(min, Math.min(max, v)); }
</script>
<input
type="range"
bind:value={() => value, (v) => (value = clamp(v))}
{min} {max}
/>
Critical: $inspect / $inspect.trace
开发时打印状态变化(生产环境为 noop):
<script>
let count = $state(0);
let message = $state('hello');
$inspect(count, message); // 变化时自动 console.log
</script>
深度追踪 —— 修改
user.name或arr.push(x)都触发。Stack trace 同步打印(playground 除外)。
$inspect(...).with() —— 自定义处理
$inspect(count).with((type, count) => {
if (type === 'update') {
debugger; // 或 console.trace、埋点等
}
});
签名:(type: 'init' | 'update', ...values) => void。
$inspect.trace()(Svelte 5.14+)
让所在函数被追踪,重跑时打印触发的状态:
<script>
$effect(() => {
// ⚠️ 必须是函数体的第一条语句
$inspect.trace();
doSomeWork();
});
</script>
可选 label:
$effect(() => {
$inspect.trace('resize-handler');
layout();
});
文档:"Any time the function re-runs as part of an effect or a derived, information will be printed to the console about which pieces of reactive state caused the effect to fire."
Critical: $host
仅在编译为自定义元素时使用,访问宿主元素:
<svelte:options customElement="my-stepper" />
<script>
function dispatch(type) {
$host().dispatchEvent(new CustomEvent(type));
}
</script>
<button => dispatch('decrement')}>-</button>
<button => dispatch('increment')}>+</button>
<!-- App.svelte -->
<script>
import './Stepper.svelte';
let count = $state(0);
</script>
<my-stepper
=> (count -= 1)}
=> (count += 1)}
></my-stepper>
典型用例
- 派发自定义事件:
$host().dispatchEvent(new CustomEvent('x', { detail, bubbles: true, composed: true })) - 访问宿主属性:
$host().getAttribute('foo')/setAttribute(...) - 调用宿主方法:
$host().focus()等 - 暴露 imperative API:用
export function
自定义元素配置
<svelte:options
customElement="my-tag"
customElementShadow="open" <!-- 或 "closed" / "none" -->
customElementEvents={['change']} <!-- 声明可冒泡事件 -->
customElementObservers={{ attr: ['data-foo'] }} <!-- 观察的 attribute -->
/>
错误用法
<!-- ❌ 普通组件(非 custom element)中调用 -->
<script>
$host(); // 编译错误
</script>
<!-- ❌ 用 $host().querySelector 找内部元素 -->
<script>
$host().querySelector('.child'); // 改用 bind:this
</script>
Quick Fixes
| 问题 | 解决方案 |
|---|---|
| 状态变化不更新 UI | 确认用了 $state(不是普通 let) |
$derived 不生效 |
依赖必须同步读取(不在 await 后) |
$effect 无限循环 |
不要在其中直接修改 $state,改用 $derived |
类方法中 this 丢失 |
用箭头函数字段或内联函数 |
| prop 变异警告 | 用 $bindable 或 callback props |
| 解构后响应式丢失 | 访问原对象属性而非解构变量 |
| Map/Set 修改不触发更新 | 改用 SvelteMap / SvelteSet |
跨文件 $state 读到 Signal 对象 |
用不可重新赋值的对象 + 导出函数 |
Gotchas
$state是深层代理 — 解构后丢失响应式,访问原始对象属性$effect不追踪异步读取 —await/setTimeout后的读取不在依赖中- 类中
$effect字段 — 方法内读取的状态不作为依赖追踪 $effect不应在 SSR 运行 — 浏览器专用,SSR 时自动跳过$props默认值不是代理 — 非$bindable的 prop fallback 值不是响应式对象- 派生返回新引用 — 即便内容相同也会触发下游更新
$inspect是开发工具 — 生产编译为 noop,不可用于业务逻辑$host仅 custom element — 普通组件中使用会编译错误- Map/Set 必须用响应式版本 — 原生 API 不会触发更新
- 跨文件 export
$state—export let重新赋值的$state不可行
FAQ
Q: $derived 和 $state 的区别?
A: $state 创建可变状态;$derived 创建只读派生值,自动从依赖推导。5.25+ 可用 let 覆盖派生实现乐观 UI。
Q: 什么时候用 $effect?
A: 仅用于副作用:DOM 操作、第三方库调用、网络请求。派生值同步状态永远不用 $effect。
Q: $state.raw 和普通 $state 的区别?
A: $state.raw 不对数组/对象创建深层代理,性能更好,但只能通过重新赋值来更新。
Q: $props 能解构吗?
A: 可以,且支持默认值、重命名、rest 解构、$bindable、$props.id()。
Q: $bindable 与函数绑定区别?
A: $bindable 是声明 prop 可双向;函数绑定 bind:value={() => v, setter} 是自定义读写逻辑。两者可组合。
Q: 何时用 $bindable vs callback props?
A: 受控表单用 $bindable;事件通知用 callback props。避免滥用 $bindable 造成数据流混乱。
Q: $inspect 在生产会运行吗?
A: 不会,生产构建编译为 noop。
Q: $effect.pending() 有什么用途?
A: 显示当前 boundary 中待定的 Promise 数量,可用于全局 loading 状态。
Q: $effect.root() 何时用?
A: 测试工具、组件外创建 effect、模块级 effect。需手动调用返回的 destroy() 清理。
Q: 跨模块共享状态用 .svelte.js 还是 Context?
A: 真正全局单例用 .svelte.js;请求级隔离/组件树共享优先 Context。
Examples
可执行的代码示例,见 examples/ 目录:
| 文件 | 内容 |
|---|---|
state-patterns.md |
$state 基础、深层代理、$state.raw/snapshot/eager、类字段、内置响应式类、跨模块 |
derived-patterns.md |
$derived 基础、$derived.by、乐观 UI、解构派生、update propagation、async |
effect-patterns.md |
$effect 基础、cleanup、$effect.pre/tracking/pending/root、禁忌、untrack |
props-patterns.md |
$props 解构、类型安全、Rest Props、$bindable、$props.id()、泛型组件 |
bindable-patterns.md |
$bindable 全部用法:双向绑定、fallback、函数绑定、数组共享 |
inspect-patterns.md |
$inspect/$inspect.trace/$inspect.with 调试模式 |
References
深入技术参考,见 references/ 目录:
| 文件 | 内容 |
|---|---|
runes-overview.md |
Runes 整体架构、vs Legacy 对比、生命周期 |
$state-deep.md |
Proxy 行为、$state.raw/snapshot/eager、类中 $state、内置响应式类、跨模块 |
$derived-deep.md |
表达式 vs $derived.by、依赖追踪、可写派生、Push-pull、解构 |
$effect-deep.md |
pre/tracking/cleanup/pending/root、追踪规则、常见错误 |
$props-deep.md |
解构、Rest Props、Type safety、$props.id()、泛型 |
$bindable-deep.md |
完整 $bindable 参考:双向绑定、fallback、函数绑定组合 |
$inspect-deep.md |
$inspect、$inspect.with、$inspect.trace 完整参考 |
$host-deep.md |
自定义元素 $host 用法、<svelte:options> 配置、典型模式 |
context-deep.md |
createContext vs setContext、类型安全、SSR |