- name
- svelte-runes
- license
- Apache-2.0
- description
- Svelte 5 Runes 响应式系统技能。当用户需要使用 $state/$derived/$effect/$props/$bindable/$inspect/$host 等符文,或理解 Svelte 5 显式响应式与 Svelte 4 隐式响应式的区别时使用。
# Svelte Runes Reference (Svelte 5)
本技能覆盖 Svelte 5 的 Runes(符文)系统。Runes 是 Svelte 5 引入的显式响应式语法,取代了 Svelte 4 的隐式 `let` 声明和 `$:` 语句。
## Capability Boundaries
### ✅ 强项
1. Svelte 5 显式响应式系统(Runes)的完整参考:$state / $derived / $effect / $props / $bindable / $inspect / $host
2. 详细的子段(Proxy 行为、依赖追踪、cleanup、pre/tracking/pending/root 等)
3. 跨模块共享状态模式 + `.svelte.js` 文件约定
4. Svelte 4 → Svelte 5 迁移提示(隐式 vs 显式响应式)
5. 完整的 Quick Fixes / Gotchas / FAQ 速查
### ⚠️ 限制
1. 本技能专注于 Svelte 5 Runes 体系,不覆盖 Svelte 4 隐式响应式(`let` + `$:`)
2. 不包含动画 / 过渡 / 模板语法的深入内容(改用 `svelte-template-syntax`)
3. 不包含组件生命周期 / stores / context 的高级用法(仅在 Svelte Runes 相关处提及)
### ❌ Out of Scope(不该用本技能的场景)
1. **Svelte 4 隐式响应式** → 不适用,改用 Svelte 4 文档
2. **模板块语法({#if}/{#each}/{#await}/{#snippet})** → 改用 `svelte-template-syntax`
3. **transition/animate** → 改用 `svelte-template-syntax`
4. **Stores(writable/derived)** → 改用 Svelte 5 文档对应章节(与 Runes 互补)
## Data Privacy
本技能不收集、存储或传输任何用户数据。所有代码示例仅用于本地开发参考。
## Workflow
Step 1. **识别用户需要的 Rune** — `$state` / `$derived` / `$effect` / `$props` / `$bindable` / `$inspect` / `$host`
Step 2. **查对应章节** — SKILL.md 给概览,深度细节见 `references/$X-deep.md`
Step 3. **必要时看示例** — `examples/$X-patterns.md` 提供可复制代码
Step 4. **查 Quick Fixes / Gotchas / FAQ** — 三个速查表覆盖常见错误
Step 5. **Svelte 4 迁移场景** — 关注"Runes Overview"对比段
## When to use this skill
当用户需要理解或使用 `$state`、`$derived`、`$effect`、`$props`、`$bindable`、`$inspect`、`$host` 等符文,或需要将 Svelte 4 代码迁移到 Svelte 5 时使用本技能。
## Critical: Runes Overview
Runes 是以 `$` 为前缀的符号,类似于函数调用语法:
```js
let message = $state('hello');
```
关键区别:
- **无需导入** — Runes 是语言内置关键字
- **不是值** — 不能赋值给变量或作为函数参数
- **位置敏感** — 仅在特定位置有效(编译器会报错)
| Rune | 用途 |
|------|------|
| `$state` | 创建响应式状态 |
| `$derived` | 声明派生计算值 |
| `$effect` | 声明副作用 |
| `$props` | 声明组件属性 |
| `$bindable` | 可绑定 prop |
| `$inspect` | 开发调试 |
| `$host` | 自定义元素访问 |
## Critical: $state
创建响应式状态,UI 在状态变化时自动更新。
```svelte
let count = $state(0);
let user = $state({ name: 'Ada', age: 30 });
```
### 深层响应式代理
`$state` 对数组和简单对象自动创建深层 Proxy,属性变更自动触发更新:
```js
let todos = $state([
{ done: false, text: 'task' }
]);
todos[0].done = true; // ✅ 触发更新
todos.push({ done: false }); // ✅ 触发更新
```
### $state.raw(避免深层代理)
适用于大数组和无需深层变更的场景:
```js
let list = $state.raw([]);
// 只能重新赋值,不能 .push()
list = [...list, newItem]; // ✅
list.push(newItem); // ❌ 无效
```
> `$state.raw` 内部仍可包含响应式状态(如原始数组里放代理对象)。
### $state.snapshot(静态快照)
获取深层代理的只读快照(用于传给外部 API):
```svelte
console.log($state.snapshot(proxyObject));
```
> 若值有 `toJSON()` 方法,snapshot 会克隆 `toJSON()` 的返回值。
### $state.eager(即时 UI 更新)
用于 `await` 表达式中立即更新 UI:
```svelte
<nav>
<a href="/" aria-current={$state.eager(pathname) === '/' ? 'page' : null}>home</a>
</nav>
```
### 类字段中的 $state
```js
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` 不响应 —— 改用响应式版本:
```js
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 行为一致):
```js
let { name, age } = $state({ name: 'Ada', age: 30 });
name = 'Bob'; // ❌ 不会触发更新,原对象不变
```
### 跨模块传递状态
`.svelte.js/.svelte.ts` 文件中可使用 Runes,但不能直接 `export let` 重新赋值的 `$state`:
```js
// ❌ 不可行:另一文件读到的会是 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:
```js
/** @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
声明派生值——基于已有状态的只读计算值。
```svelte
let count = $state(0);
let doubled = $derived(count * 2);
```
> 表达式**必须纯净** —— 内部不能修改 `$state`(编译器报错)。
### $derived.by(复杂派生)
```svelte
let total = $derived.by(() => {
let sum = 0;
for (const item of items) sum += item.price;
return sum;
});
```
### 派生值覆盖(Svelte 5.25+,乐观 UI)
```svelte
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` 都是依赖:
```js
let a = Promise.resolve(1);
let b = 2;
let sum = $derived(await a + b);
// a 和 b 都是依赖(await 之后的同步读取也会追踪)
```
> 仅**表达式自身**的 `await` 后的同步读才算依赖;**调用函数**内部的 `await` 不计入。
用 `untrack` 排除非依赖值。
### Push-pull 响应
派生值**只在被读取时**重新计算(pull),但状态变化时**立即通知**所有依赖(push)。若派生返回的引用未变,下游不更新:
```svelte
let count = $state(0);
let large = $derived(count > 10); // 布尔
// 大文本节点只在 large 变化时重渲染,而非 count
```
> 派生内避免 `() => ({})`、`[...].map(...)` 这类返回新引用的写法 —— 会让下游始终重算。
### 派生不解构代理
`$derived` 不会把返回值包成 Proxy —— 修改派生返回对象的属性会影响到底层 `$state`:
```js
let items = $state([...]);
let selected = $derived(items[0]);
selected.name = 'new'; // ✅ 影响 items[0].name
```
### 派生解构
```js
let { first, last } = $derived(user);
// 等价于:
// let first = $derived(user.first);
// let last = $derived(user.last);
```
## Critical: $effect
声明副作用——DOM 操作、第三方库调用、网络请求等。**不应**用 `$effect` 同步状态。
```svelte
$effect(() => {
document.title = `count: ${count}`;
return () => { /* 清理函数 */ };
});
```
### 生命周期
- 挂载后**首次执行**
- 状态变化时 **microtask 调度**(批量)
- DOM 更新**之后**执行
- 仅浏览器执行(SSR 自动跳过)
- 可在组件任意位置调用,只要在父 effect 运行期间
### 依赖追踪
自动追踪 `$state`/`$derived` 的同步读取:
```svelte
$effect(() => {
// color 和 size 是依赖
ctx.fillStyle = color;
ctx.fillRect(0, 0, size, size);
});
```
### 异步不追踪
`await` 之后和 `setTimeout` 内部的读取**不**追踪:
```js
$effect(() => {
ctx.fillStyle = color; // ✅ 追踪
setTimeout(() => {
ctx.fillRect(0, 0, size, size); // ❌ size 不追踪
}, 0);
});
```
### 条件分支决定依赖
effect **只追踪上次运行时实际读取的状态**:
```ts
$effect(() => {
if (condition) {
confetti({ colors: [color] });
} else {
confetti();
}
});
// condition=false 时 color 不再是依赖
```
### $effect.pre(DOM 更新前运行)
```svelte
$effect.pre(() => {
if (!div) return; // 挂载前跳过
messages.length; // 显式依赖追踪
// DOM 更新前执行(如滚动位置计算)
});
```
### $effect.tracking(判断追踪上下文)
```svelte
console.log($effect.tracking()); // false(组件初始化)
$effect(() => {
console.log($effect.tracking()); // true(effect 内)
});
```
> 用于 `createSubscriber` 这类工具:仅在状态被追踪时订阅,事件处理器中不订阅。
### $effect.pending()(待定 Promise 数量)
返回**当前 boundary**(不含子 boundary)中待定的 Promise 数量:
```svelte
<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}
```
### $effect.root()(独立作用域)
创建**非追踪**作用域,不随组件销毁自动清理:
```js
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();
```
Auf GitHub ansehen