$state 进阶
本教程共 50 篇 · 第 6 篇 · 更新于 2026-08-05 · 约 6 分钟阅读
本节目标:掌握 $state.snapshot、$state.raw 的用法,学会在 .svelte.js 模块和 class 中使用 $state,理解跨模块共享状态的限制。学完你能应对复杂的状态管理场景。
$state.snapshot:获取不可变快照
$state 创建的深层响应代理(Proxy)在很多时候很好用,但有些场景下你需要拿到一个没有代理包装的原始对象。比如你要把状态传给一个不认识 Proxy 的外部库,或者用 structuredClone 深拷贝。
$state.snapshot 就是干这个的:
<script>
let counter = $state({ count: 0 });
function logSnapshot() {
// 输出 { count: 0 },而不是 Proxy { ... }
console.log($state.snapshot(counter));
}
function cloneState() {
// structuredClone 不认识 Proxy,需要先 snapshot
const copy = structuredClone($state.snapshot(counter));
console.log(copy);
}
</script>
<button onclick={() => counter.count++}>加一</button>
<button onclick={logSnapshot}>打印快照</button>
<button onclick={cloneState}>克隆状态</button>
Note快照是深拷贝,修改快照不会影响原始状态。如果对象有
toJSON方法,快照会克隆toJSON()的返回值。
$state.raw:浅响应模式
默认情况下,$state 对对象和数组做深层代理。但如果你有一个很大的数组或对象,而且你不打算修改它的属性(只会整体替换),深层代理就是浪费性能。
$state.raw 解决这个问题——它创建不会被代理的状态,只能整体重新赋值来触发更新:
<script>
let person = $state.raw({
name: '码上学',
age: 25
});
function growUp() {
// 这样不会触发更新!
person.age += 1;
}
function celebrateBirthday() {
// 这样会触发更新——整体替换新对象
person = {
name: '码上学',
age: person.age + 1
};
}
</script>
<p>{person.name},{person.age}岁</p>
<button onclick={celebrateBirthday}>过生日</button>
| 对比项 | $state | $state.raw |
|---|---|---|
| 深层代理 | 有 | 没有 |
| 属性修改触发更新 | 是 | 否 |
| 整体替换触发更新 | 是 | 是 |
| 性能 | 较低(代理开销) | 较高(无代理) |
| 适用场景 | 需要细粒度更新 | 大数据、不可变模式 |
Tip
$state.raw类似 React 中的useState——只能整体替换,不能直接修改属性。如果你习惯不可变数据的写法,用$state.raw会很自然。
在 class 中使用 $state
Class 实例不会被代理。但你可以把 $state 用在 class 的字段上,让实例属性变成响应式的:
class Todo {
done = $state(false);
constructor(text) {
this.text = $state(text);
}
reset() {
this.text = '';
this.done = false;
}
}
这样创建的 Todo 实例,其 done 和 text 属性都是响应式的。修改它们会触发 UI 更新。
Note编译器会把
done和text转换成 class 原型上的get/set方法,指向私有字段。所以这些属性不会出现在Object.keys()中(不可枚举)。
this 绑定问题
在 class 方法中使用 this 时要注意绑定问题。直接把方法传给事件处理器,this 会丢失:
<!-- 这样不行,this 指向 <button> 而不是 Todo 实例 -->
<button onclick={todo.reset}>重置</button>
两种解决方式:
<!-- 方式一:用内联箭头函数 -->
<button onclick={() => todo.reset()}>重置</button>
// 方式二:在 class 中用箭头函数定义方法
class Todo {
done = $state(false);
constructor(text) {
this.text = $state(text);
}
// 箭头函数自动绑定 this
reset = () => {
this.text = '';
this.done = false;
}
}
在 .svelte.js/.svelte.ts 中使用 $state
.svelte.js 和 .svelte.ts 文件支持 Runes,所以你可以在里面创建共享的响应式状态:
//--- file: state.svelte.js ---
// ✅ 正确:不重新赋值,只修改属性
export const counter = $state({
count: 0
});
export function increment() {
counter.count += 1;
}
export function reset() {
counter.count = 0;
}
在组件中使用:
<script>
import { counter, increment, reset } from './state.svelte.js';
</script>
<p>计数:{counter.count}</p>
<button onclick={increment}>加一</button>
<button onclick={reset}>重置</button>
跨模块导出状态的限制
有一个重要限制:不能跨模块导出会被重新赋值的 $state。
//--- file: state.svelte.js ---
// ❌ 错误!导出的 count 会被重新赋值
export let count = $state(0);
export function increment() {
count += 1; // 重新赋值
}
为什么不行?因为 Svelte 编译器在编译 count += 1 时,会把它转换成 $.set(count, $.get(count) + 1)。但其他文件导入 count 时,编译器不会对导入的变量做这种转换,所以拿到的是 Signal 对象而不是数字。
两种正确的做法:
做法一:不重新赋值,只改属性
// ✅ 导出对象,修改属性而不是重新赋值
export const counter = $state({ count: 0 });
export function increment() {
counter.count += 1;
}
做法二:不直接导出状态,通过函数访问
// ✅ 不导出 state 变量,导出访问函数
let count = $state(0);
export function getCount() {
return count;
}
export function increment() {
count += 1;
}
Tip做法一更简洁,做法二封装性更好。根据项目需求选择。记住核心原则:导出的状态不要重新赋值,只修改属性。
内置响应式类
Svelte 提供了 Set、Map、Date、URL 等内置类的响应式实现,可以从 svelte/reactivity 导入:
<script>
import { SvelteSet } from 'svelte/reactivity';
let tags = new SvelteSet(['前端', 'Svelte']);
function toggleTag(tag) {
if (tags.has(tag)) {
tags.delete(tag);
} else {
tags.add(tag);
}
}
</script>
<p>标签:{[...tags].join(', ')}</p>
<button onclick={() => toggleTag('TypeScript')}>切换 TypeScript</button>
原生 Set 的 add/delete 不会触发 $state 更新,但 SvelteSet 会。
本节回顾
$state.snapshot()获取状态的不可变深拷贝,传给外部库时用$state.raw()创建浅响应状态,只能整体替换,适合大数据和不可变模式- class 字段可以用
$state,编译为 get/set + 私有字段 - class 方法中的
this需要用箭头函数绑定 .svelte.js模块中可创建共享响应式状态,但不能导出会被重新赋值的$statesvelte/reactivity提供了SvelteSet、SvelteMap等响应式内置类