Svelte 响应式
约 1739 字大约 6 分钟
2025-11-15
响应式系统
let count = $state(0);
function increment() {
count += 1;
}Svelte 会:
- 自动追踪对
count的读取与修改 - 自动重新更新所有使用
count的 DOM - 不需要手动
.set()或this.setState()或ref.value
假设你的模板是:
<p>{count}</p>
<button onclick={increment}>Increment</button>点击按钮后 DOM 会自动更新,无需手动触发刷新。
let count = $state(0)不是一个运行时函数,而是 编译器指令。
Svelte 编译器看到 $state() 会按以下方式变形:
- 编译成一个内部的 reactive store
- 追踪变量的依赖
- 生成优化后的 DOM 更新代码(不是虚拟 DOM)
深度响应性
let numbers = $state([1, 2, 3, 4]);这个数组是 响应式代理对象(Proxy)。
这意味着:对它的 任何修改 push、splice、直接赋值、修改对象属性……都会触发 DOM 更新。
Svelte 会响应所有深层修改
numbers[0] = 999; // 会更新
numbers.push(10); // 会更新
numbers.length = 0; // 会更新不仅数组,对象也一样:
let user = $state({ name: 'A', age: 20 });
function grow() {
user.age++; // 会触发更新
}嵌套对象也有深度响应:无限深度
let state = $state({
user: {
profile: {
name: "Alice"
}
}
});
state.user.profile.name = "Bob"; // 自动更新 UISvelte 使用 Proxy 递归包装所有内容,直到访问发生。
Map / Set 也是响应式的
let set = $state(new Set([1, 2, 3]));
set.add(4); // ✔ 更新
set.delete(2); // ✔ 更新Map 同理:
let map = $state(new Map());
map.set("x", 10); // ✔ 更新这是 Vue 3 才支持的,但 Svelte 一样实现了。
React 完全不支持,请求更新必须重新创建。
注意:深度响应与原始对象分离
let arr = [1,2,3];
let numbers = $state(arr);numbers 是 Proxy,但 arr 不是 Proxy。
修改 arr 不会触发更新,修改 numbers 才会。
Svelte 为什么能实现深度响应?
因为:
- 每个
$state()返回 本质上是 Proxy - 所有子对象在首次访问时会递归代理
- 任何 “set / delete / mutation” 都会被拦截
然后 Svelte 会:
- 标记组件需要更新
- 执行编译后的 DOM diff(极小 diff)
- 最终更新 DOM
这是 编译 + 代理 的组合。
$state 不需要用 $state([]) 二次包裹属性
下面是错误方式:
let user = $state({
name: $state("Alice"), // 不需要
});正确方式:
let user = $state({
name: "Alice", // 组件底层会自动代理
});Svelte 会自动递归代理内部对象。
派生状态
Svelte 的 $derived 与 Vue 的 computed(计算属性)本质上是同一种思想,但 Svelte 的实现更底层、更轻量、更强大一些。
基本用法:
let numbers = $state([1, 2, 3, 4]);
let total = $derived(numbers.reduce((t, n) => t + n, 0));然后你可以在模板中引用:
<p>{numbers.join(" + ")} = {total}</p>每当 numbers 改变:
Svelte 自动重新执行 $derived(...)
total 会得到最新值
DOM 会更新
关键点:$derived 是只读的,不能手动修改。
$derived 与 Vue computed 的对比
| 功能点 | Svelte $derived | Vue computed |
|---|---|---|
| 定义方式 | $derived(expr) | computed(() => expr) |
| 是否只读 | ✔ 只读 | 默认只读(也可定义 setter) |
| 自动依赖追踪 | ✔ 自动 | ✔ 自动 |
| 基于 Proxy | ✔ 是 | ✔ 是(Vue 3) |
| 触发时机 | 依赖变化实时更新 | 依赖变化按需更新(缓存) |
| 是否缓存 | ❌ 不缓存(再次访问会重新执行) | ✔ 有缓存 |
重大区别:Svelte 的派生值不缓存!
在 Svelte 中,每次渲染都会执行 $derived(...):
let total = $derived(numbers.reduce(...));它会在:
- numbers 变化时更新
- 组件重新渲染时重新调用表达式(非缓存)
Vue 的 computed 会缓存,直到依赖变化才重新计算。
那 Svelte 为什么不缓存?
因为 Svelte 是 编译时框架,编译器已经知道依赖关系,它不需要在运行时做缓存优化,这样能:
- 让运行时更轻量
- 减少响应系统的复杂度
- 让依赖追踪更直接
大多数派生值计算非常轻量,所以不缓存影响很小。
$derived 的限制:
不能手动修改,不能放异步逻辑
let data = $derived(await fetch(...)); // 不允许 async要用 $effect 或 await block
不应该做重运算(因为不缓存),如果真的很大计算量,可以搭配 $effect, $state.snapshot() 等手动优化。
总结:$derived 是 Svelte 中用于从其他响应式状态自动计算只读派生数据的机制,功能近似 Vue 的 computed,但不会缓存,依赖由编译器静态确定,运行时极其轻量。
状态调试
Svelte 在运行时的响应式状态是通过 Proxy(代理) 实现的,这意味着如果你直接把某个状态对象打印到控制台:
console.log(numbers);你会看到警告或无法克隆的提示,因为 Proxy 对象不能被结构化克隆(structured clone),为了解决这个问题,Svelte 提供了两种调试方式:
$state.snapshot(...) 获取非响应式快照,把响应式 Proxy 转换为普通对象/数组。
function addNumber() {
numbers.push(numbers.length + 1);
// 打印快照
console.log($state.snapshot(numbers));
}作用:
- 去掉 Proxy 外壳
- 得到真正的数组/对象
- 可以安全打印、存储、发送给后端
这种方式适合在函数内部临时调试。
$inspect(...) 自动追踪状态变化(最推荐)
Svelte 的响应式系统会在代理对象 发生任意变化时 自动触发 $inspect 回调
$inspect(numbers);你什么都不需要做,只要 numbers 变化,控制台就自动打印快照。
$inspect 的特点
- 无需手动调试,它会在状态变化时自动输出快照
- 不会影响性能
- 生产环境会自动移除
- 适合开发调试复杂状态
$inspect(...).with(fn) 自定义调试行为
你可以让 $inspect 调用你自己的函数,比如 console.trace:
$inspect(numbers).with(console.trace);效果:每次 numbers 变化时打印堆栈跟踪,以便找到「是谁修改了状态」。
你也可以写成:
$inspect(numbers).with(value => {
console.log("numbers changed:", value);
});或者:
$inspect(numbers).with(v => console.log("snapshot:", JSON.stringify(v)));$state.snapshot 与 $inspect 的区别总结
| 功能 | $state.snapshot | $inspect |
|---|---|---|
| 是否自动触发 | ❌ 需要手动调用 | ✔ 任何变更自动触发 |
| 使用场景 | 函数内部、单次打印 | 调试深度状态、开发期监控 |
| 数据是否是快照 | ✔ 是 | ✔ 是 |
| 生产环境保留吗 | ✔ 保留 | ❌ 自动移除 |
| 自定义显示方式 | ❌ | ✔ .with(fn) |
场景:你想监控一个大型全局 store 的变动
// store.js
export let user = $state({
id: 1,
name: "Yumeng",
age: 18
});
// 调试
$inspect(user);一旦 user 内部属性改变:
user.age = 19;浏览器立刻打印:
{ id: 1, name: "Yumeng", age: 19 }场景:你想知道某个状态是哪段代码改的
$inspect(numbers).with(console.trace);控制台会告诉你:
numbers changed
at addNumber()
at ...这在调试复杂逻辑时非常强大。
总结
$state.snapshot用于把响应式 Proxy 转换成普通对象,便于打印或传输。$inspect用于自动追踪状态变化,在开发中提供快照调试,生产环境自动移除。$inspect(...).with(fn)可以自定义调试行为,如console.log或console.trace。