$state

$state rune 让你可以创建_响应式状态(reactive state),也就是说,当状态发生变化时,你的 UI 也会随之_响应

<!--- file: App.svelte --->
<script>
	function greet() {
		alert('Welcome to Svelte!');
	}
</script>

<button onclick={greet}>click me</button>

<style>
	button {
		font-size: 2em;
	}
</style>

与其他你可能接触过的框架不同,这里没有任何用于操作状态的 API——count 只是一个数字,而不是一个对象或函数,你可以像更新其他任何变量一样去更新它。

深层状态(Deep state)

如果 $state 与一个数组或普通对象一起使用,结果会是一个深度响应式的_状态代理(state proxy)_。Proxy 允许 Svelte 在你读取或写入属性(包括通过 array.push(...) 这类方法)时运行代码,从而触发细粒度的更新。

状态会被递归地代理化,直到 Svelte 遇到数组或普通对象以外的东西(比如类,或通过 Object.create 创建的对象)。在这种情况下……

let todos = $state([
	{
		done: false,
		text: 'add more todos'
	}
]);

……修改某个 todo 的属性也会触发 UI 中依赖该特定属性的部分更新:

let todos = [{ done: false, text: 'add more todos' }];
todos[0].done = !todos[0].done;

如果你向数组中 push 一个新对象,它也会被代理化:

let todos = [{ done: false, text: 'add more todos' }];
todos.push({
	done: false,
	text: 'eat lunch'
});

[!NOTE] 当你更新代理的属性时,原始对象_并不会_被修改。如果你需要在自己的状态代理中使用自定义的代理处理器,你应该在用 $state 包裹对象_之后_再包裹它

注意,如果你对响应式值进行解构,得到的引用并不是响应式的——就像在普通 JavaScript 中一样,它们是在解构那一刻被求值的:

let todos = [{ done: false, text: 'add more todos' }];
let { done, text } = todos[0];

// 这不会影响 `done` 的值
todos[0].done = !todos[0].done;

类(Classes)

类的实例不会被代理。作为替代,你可以在类的字段(无论是公有还是私有)中使用 $state,或者在 constructor 内部第一次给属性赋值时使用它:

class Todo {
	done = $state(false);

	constructor(text) {
		this.text = $state(text);
	}

	reset() {
		this.text = '';
		this.done = false;
	}
}

[!NOTE] 编译器会把 donetext 转换为类原型上引用私有字段的 get/set 方法。这意味着这些属性不可枚举。

在 JavaScript 中调用方法时,this 的值很重要。下面这样写是行不通的,因为 reset 方法内部的 this 会是 <button> 而不是 Todo

<button onclick={todo.reset}>
	reset
</button>

你可以改用内联函数……

<button onclick={() => todo.reset()}>
	reset
</button>

……或者在类定义中使用箭头函数:

class Todo {
	done = $state(false);

	constructor(text) {
		this.text = $state(text);
	}

	reset = () => {
		this.text = '';
		this.done = false;
	}
}

内置类(Built-in classes)

Svelte 提供了 SetMapDateURL 等内置类的响应式实现,你可以从 svelte/reactivity 中导入它们。

$state.raw

如果你不希望对象与数组被深度响应式化,可以使用 $state.raw

$state.raw 声明 state 后,它不能被修改(mutate),只能被_重新赋值(reassign)_。换句话说,与其给对象的属性赋值,或使用像 push 这样的数组方法,不如在需要更新时整体替换该对象或数组:

let person = $state.raw({
	name: 'Heraclitus',
	age: 49
});

// 这不会有任何效果
person.age += 1;

// 这会生效,因为我们创建了一个新的 person
person = {
	name: 'Heraclitus',
	age: 50
};

这可以在处理本来就不打算修改的大数组和大对象时提升性能,因为它避免了将它们变为响应式状态的代价。注意,原始状态可以_包含_响应式状态(例如,一个由响应式对象组成的原始数组)。

$state 一样,你也可以使用 $state.raw 来声明类字段。

$state.snapshot

要获取深度响应式的 $state 代理的静态快照,请使用 $state.snapshot

<script>
	let counter = $state({ count: 0 });

	function onclick() {
		// 会输出 `{ count: ... }` 而不是 `Proxy { ... }`
		console.log($state.snapshot(counter));
	}
</script>

当你想把某些状态传递给不期望接收代理的外部库或 API(例如 structuredClone)时,这会很方便。

如果一个值带有 toJSON 方法,快照会克隆从该 toJSON 返回的值,而不是原始对象。

$state.eager

当状态发生变化时,如果它被某个 await 表达式使用,它可能不会立即反映在 UI 中,因为更新是同步协调的

在某些情况下,你可能希望状态一变化就更新 UI。例如,当用户点击链接时,你可能想更新导航栏,以便他们在等待新页面加载时获得视觉反馈。为此,请使用 $state.eager(value)

<nav>
	<a href="/" aria-current={$state.eager(pathname) === '/' ? 'page' : null}>home</a>
	<a href="/about" aria-current={$state.eager(pathname) === '/about' ? 'page' : null}>about</a>
</nav>

请谨慎使用此特性,并且只在响应用户操作时提供反馈——通常情况下,让 Svelte 来协调更新会带来更好的用户体验。

将状态传入函数

JavaScript 是一种_按值传递(pass-by-value)的语言——当你调用函数时,参数传递的是_值_而不是_变量。换句话说:

/**
 * @param {number} a
 * @param {number} b
 */
function add(a, b) {
	return a + b;
}

let a = 1;
let b = 2;
let total = add(a, b);
console.log(total); // 3

a = 3;
b = 4;
console.log(total); // 仍然是 3!

如果 add 想要访问 ab 的_当前_值,并返回 total 的当前值,你就需要使用函数:

/**
 * @param {() => number} getA
 * @param {() => number} getB
 */
function add(getA, getB) {
	return () => getA() + getB();
}

let a = 1;
let b = 2;
let total = add(() => a, () => b);
console.log(total()); // 3

a = 3;
b = 4;
console.log(total()); // 7

Svelte 中的状态也不例外——当你引用用 $state rune 声明的内容时……

let a = $state(1);
let b = $state(2);

……你访问的是它的_当前值_。

注意,"函数" 一词含义很广——它涵盖了代理的属性以及 get/set 属性……

/**
 * @param {{ a: number, b: number }} input
 */
function add(input) {
	return {
		get value() {
			return input.a + input.b;
		}
	};
}

let input = $state({ a: 1, b: 2 });
let total = add(input);
console.log(total.value); // 3

input.a = 3;
input.b = 4;
console.log(total.value); // 7

……不过,如果你发现自己写的是这样的代码,不妨考虑改用

跨模块共享状态

你可以在 .svelte.js.svelte.ts 文件中声明状态,但你只能导出那些没有被重新赋值的状态。换句话说,你不能这样做:

export let count = $state(0);

export function increment() {
	count += 1;
}

这是因为对 count 的每次引用都会被 Svelte 编译器转换——上面的代码大致等价于:

interface Signal<T> {
	value: T;
}

interface Svelte {
	state<T>(value?: T): Signal<T>;
	get<T>(source: Signal<T>): T;
	set<T>(source: Signal<T>, value: T): void;
}
declare const $: Svelte;
export let count = $.state(0);

export function increment() {
	$.set(count, $.get(count) + 1);
}

[!NOTE] 你可以点击演练场中的 "JS Output" 选项卡查看 Svelte 生成的代码。

由于编译器一次只处理一个文件,如果另一个文件导入了 count,Svelte 并不知道它需要用 $.get$.set 包裹每一次引用:

export let count = 0;

import { count } from './state.svelte.js';

console.log(typeof count); // 'object',而不是 'number'

因此你有两种选择来在模块之间共享状态——要么不去重新赋值它……

// 这是允许的——因为我们更新的是 `counter.count` 而不是 `counter`,
// Svelte 不会把它包裹进 `$.state`
export const counter = $state({
	count: 0
});

export function increment() {
	counter.count += 1;
}

……要么干脆不要直接导出它:

let count = $state(0);

export function getCount() {
	return count;
}

export function increment() {
	count += 1;
}