svelte-runes
Svelte 5 Runes 响应式系统技能。当用户需要使用 $state/$derived/$effect/$props/$bindable/$inspect/$host 等符文,或理解 Svelte 5 显式响应式与 Svelte 4 隐式响应式的区别时使用。
Svelte 5 Runes 响应式系统技能。当用户需要使用 $state/$derived/$effect/$props/$bindable/$inspect/$host 等符文,或理解 Svelte 5 显式响应式与 Svelte 4 隐式响应式的区别时使用。
本技能覆盖 Svelte 5 的 Runes(符文)系统。Runes 是 Svelte 5 引入的显式响应式语法,取代了 Svelte 4 的隐式 let 声明和 $: 语句。
当用户需要理解或使用 $state、$derived、$effect、$props、$bindable、$inspect、$host 等符文,或需要将 Svelte 4 代码迁移到 Svelte 5 时使用本技能。
Runes 是以 $ 为前缀的符号,类似于函数调用语法:
let message = $state('hello');
关键区别:
| Rune | 用途 |
|------|------|
| $state | 创建响应式状态 |
| $derived | 声明派生计算值 |
| $effect | 声明副作用 |
| $props | 声明组件属性 |
| $bindable | 可绑定 prop |
| $inspect | 开发调试 |
| $host | 自定义元素访问 |
创建响应式状态,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 }); // ✅ 触发更新
适用于大数组和无需深层变更的场景:
let list = $state.raw([]);
// 只能重新赋值,不能 .push()
list = [...list, newItem]; // ✅
list.push(newItem); // ❌ 无效
$state.raw内部仍可包含响应式状态(如原始数组里放代理对象)。
获取深层代理的只读快照(用于传给外部 API):
console.log($state.snapshot(proxyObject));
若值有
toJSON()方法,snapshot 会克隆toJSON()的返回值。
用于 await 表达式中立即更新 UI:
<nav>
<a href="/" aria-current={$state.eager(pathname) === '/' ? 'page' : null}>home</a>
</nav>
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()。
普通 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; }
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."
声明派生值——基于已有状态的只读计算值。
let count = $state(0);
let doubled = $derived(count * 2);
表达式必须纯净 —— 内部不能修改
$state(编译器报错)。
let total = $derived.by(() => {
let sum = 0;
for (const item of items) sum += item.price;
return sum;
});
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 排除非依赖值。
派生值只在被读取时重新计算(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);
声明副作用——DOM 操作、第三方库调用、网络请求等。不应用 $effect 同步状态。
$effect(() => {
document.title = `count: ${count}`;
return () => { /* 清理函数 */ };
});
自动追踪 $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(() => {
if (!div) return; // 挂载前跳过
messages.length; // 显式依赖追踪
// DOM 更新前执行(如滚动位置计算)
});
console.log($effect.tracking()); // false(组件初始化)
$effect(() => {
console.log($effect.tracking()); // true(effect 内)
});
用于
createSubscriber这类工具:仅在状态被追踪时订阅,事件处理器中不订阅。
返回当前 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 onclick={() => a++}>a++</button>
<button onclick={() => b++}>b++</button>
<p>{a} + {b} = {await add(a, b)}</p>
{#if $effect.pending()}
<span>pending: {$effect.pending()}</span>
{/if}
创建非追踪作用域,不随组件销毁自动清理:
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."
$effect(() => {
const interval = setInterval(() => count += 1, 1000);
return () => clearInterval(interval); // 清理
});
teardown 在 (a) effect 重跑前、(b) 组件销毁时执行。
| 需求 | 正确方案 |
|------|----------|
| 派生计算值 | $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."
声明组件属性(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)。
let { adjective = 'happy', count = 0 } = $props();
默认值不是响应式代理 —— 父组件未传时修改其属性不触发更新。文档:"Fallback values are not turned into reactive state proxies."
let { super: hero = 'default' } = $props();
let { 'class': className, 'for': htmlFor } = $props();
Props 在父组件变化时自动更新,但子组件不应直接修改 prop(除 $bindable 外):
// ❌ 不要修改
let { object } = $props();
object.count += 1; // 警告:ownership_invalid_mutation
// ✅ 用 callback props 或 $bindable
子组件可以临时重新赋值 prop(unsaved 状态),父级不受影响。
let { variant = 'primary', size = 'md', ...rest } = $props();
<button class="btn btn-{variant} btn-{size}" {...rest}>
<slot />
</button>
类型:
{ variant?: 'primary' | 'secondary'; [key: string]: any }或HTMLButtonAttributes。
生成当前组件实例唯一的 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}
允许子组件修改父组件状态的 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}
/>
开发时打印状态变化(生产环境为 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(count).with((type, count) => {
if (type === 'update') {
debugger; // 或 console.trace、埋点等
}
});
签名:(type: 'init' | 'update', ...values) => void。
让所在函数被追踪,重跑时打印触发的状态:
<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."
仅在编译为自定义元素时使用,访问宿主元素:
<svelte:options customElement="my-stepper" />
<script>
function dispatch(type) {
$host().dispatchEvent(new CustomEvent(type));
}
</script>
<button onclick={() => dispatch('decrement')}>-</button>
<button onclick={() => dispatch('increment')}>+</button>
<!-- App.svelte -->
<script>
import './Stepper.svelte';
let count = $state(0);
</script>
<my-stepper
ondecrement={() => (count -= 1)}
onincrement={() => (count += 1)}
></my-stepper>
$host().dispatchEvent(new CustomEvent('x', { detail, bubbles: true, composed: true }))$host().getAttribute('foo') / setAttribute(...)$host().focus() 等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>
| 问题 | 解决方案 |
|------|----------|
| 状态变化不更新 UI | 确认用了 $state(不是普通 let) |
| $derived 不生效 | 依赖必须同步读取(不在 await 后) |
| $effect 无限循环 | 不要在其中直接修改 $state,改用 $derived |
| 类方法中 this 丢失 | 用箭头函数字段或内联函数 |
| prop 变异警告 | 用 $bindable 或 callback props |
| 解构后响应式丢失 | 访问原对象属性而非解构变量 |
| Map/Set 修改不触发更新 | 改用 SvelteMap / SvelteSet |
| 跨文件 $state 读到 Signal 对象 | 用不可重新赋值的对象 + 导出函数 |
$state 是深层代理 — 解构后丢失响应式,访问原始对象属性$effect 不追踪异步读取 — await/setTimeout 后的读取不在依赖中$effect 字段 — 方法内读取的状态不作为依赖追踪$effect 不应在 SSR 运行 — 浏览器专用,SSR 时自动跳过$props 默认值不是代理 — 非 $bindable 的 prop fallback 值不是响应式对象$inspect 是开发工具 — 生产编译为 noop,不可用于业务逻辑$host 仅 custom element — 普通组件中使用会编译错误$state — export let 重新赋值的 $state 不可行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/ 目录:
| 文件 | 内容 |
|------|------|
| 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/ 目录:
| 文件 | 内容 |
|------|------|
| 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 |