首页 / Svelte 5 入门教程 / $state 进阶

Svelte 5 入门教程

$state 进阶

本教程共 50 篇 · 第 6 篇 · 更新于 2026-08-05 · 约 6 分钟阅读

SvelteSvelte 5Runes$state进阶

本节目标:掌握 $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 实例,其 donetext 属性都是响应式的。修改它们会触发 UI 更新。

Note

编译器会把 donetext 转换成 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 提供了 SetMapDateURL 等内置类的响应式实现,可以从 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>

原生 Setadd/delete 不会触发 $state 更新,但 SvelteSet 会。

本节回顾

  • $state.snapshot() 获取状态的不可变深拷贝,传给外部库时用
  • $state.raw() 创建浅响应状态,只能整体替换,适合大数据和不可变模式
  • class 字段可以用 $state,编译为 get/set + 私有字段
  • class 方法中的 this 需要用箭头函数绑定
  • .svelte.js 模块中可创建共享响应式状态,但不能导出会被重新赋值的 $state
  • svelte/reactivity 提供了 SvelteSetSvelteMap 等响应式内置类