$state() 深入解析
基本用法与类型推断
$state() 是 Svelte 5 响应式系统的核心。它创建一个响应式状态变量,当变量的值改变时,所有依赖它的模板和派生值会自动更新。与 React 的 useState 不同,$state 使用普通赋值语法更新,不需要 setter 函数。
<script lang="ts">
// 原始类型:TypeScript 自动推断类型
let count = $state(0); // number
let name = $state(''); // string
let active = $state(false); // boolean
// 显式类型注解
let items = $state<string[]>([]);
let user = $state<{ name: string; age: number } | null>(null);
// 普通赋值即可更新(不需要 setState)
function increment() {
count++; // 直接修改!
}
function setName(n: string) {
name = n;
}
</script>
响应式对象与数组
当 $state() 包裹对象或数组时,Svelte 会使用 Proxy 使其内部属性也具有深层响应式。这解决了 Svelte 4 中数组变更不被追踪的痛点。
<script lang="ts">
// 响应式对象——深层属性变更会触发更新
let profile = $state({
name: 'Alice',
address: {
city: '北京',
district: '朝阳区'
}
});
// 深层修改:直接赋值即可
function changeCity() {
profile.address.city = '上海'; // 自动触发更新!
}
// 响应式数组
let todos = $state([
{ id: 1, text: '学习 Svelte', done: false },
{ id: 2, text: '构建项目', done: false }
]);
// 所有数组方法都能触发响应式更新
function addTodo(text: string) {
todos.push({ id: Date.now(), text, done: false });
}
function toggleTodo(id: number) {
const todo = todos.find(t => t.id === id);
if (todo) todo.done = !todo.done; // 深层修改也行!
}
function removeTodo(id: number) {
const index = todos.findIndex(t => t.id === id);
if (index !== -1) todos.splice(index, 1);
}
</script>
{#each todos as todo (todo.id)}
<div class:done={todo.done}>
<input type="checkbox" checked={todo.done}
onchange={() => toggleTodo(todo.id)} />
{todo.text}
</div>
{/each}
$state.raw() — 非深层响应式
当你不需要深层响应式时,可以使用 $state.raw() 获得性能更好的浅层响应式:
<script lang="ts">
// $state.raw:只在整体重赋值时触发更新
// 适合大型只读数据集
let bigDataset = $state.raw(fetchData());
function refresh() {
bigDataset = fetchData(); // 整体替换才触发更新
// bigDataset.someProperty = x; // 这不会触发更新
}
</script>
$derived() 派生状态
基本派生
$derived() 用于声明从其他响应式状态自动计算的值。当依赖的状态变化时,派生值会自动重新计算。它是只读的——你不能直接赋值给派生变量。
<script lang="ts">
let price = $state(100);
let quantity = $state(3);
let discount = $state(0.1); // 10% 折扣
// 简单派生
let subtotal = $derived(price * quantity);
let discountAmount = $derived(subtotal * discount);
let total = $derived(subtotal - discountAmount);
// 复杂派生(用 $derived.by 传入函数)
let formattedTotal = $derived.by(() => {
return new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: 'CNY'
}).format(total);
});
</script>
<p>小计:{subtotal} 元</p>
<p>折扣:-{discountAmount} 元</p>
<p>总计:{formattedTotal}</p>
$effect() 副作用
基本副作用
$effect() 用于在响应式状态变化时执行副作用——如 DOM 操作、网络请求、订阅外部数据源、浏览器 API 调用等。Svelte 会自动追踪 effect 函数中访问的响应式变量,当这些变量变化时自动重新运行 effect。
<script lang="ts">
let query = $state('');
let results = $state<string[]>([]);
let loading = $state(false);
// $effect 自动追踪 query 依赖
$effect(() => {
if (!query) {
results = [];
return;
}
loading = true;
// 防抖:取消上次的请求(通过返回清理函数)
const timer = setTimeout(async () => {
const res = await fetch(`/api/search?q=${query}`);
results = await res.json();
loading = false;
}, 300);
// 清理函数:effect 重新运行前执行
return () => {
clearTimeout(timer);
loading = false;
};
});
</script>
Runes 与旧版 $: 语法对比
| 功能 | Svelte 4 ($:) | Svelte 5 (Runes) |
|---|---|---|
| 响应式状态 | let count = 0(魔法变量) | let count = $state(0) |
| 派生值 | $: doubled = count * 2 | let doubled = $derived(count * 2) |
| 副作用 | $: console.log(count) | $effect(() => { console.log(count) }) |
| Props | export let value | let { value } = $props() |
| 可跨文件 | 否(只在组件内) | 是(可写在普通 .ts 文件) |
| 类型安全 | 有限 | 完整 TypeScript 支持 |
跨文件共享响应式状态
Runes 最大的优势之一是可以将响应式逻辑提取到普通 TypeScript 文件中,在多个组件间共享:
// src/lib/stores/cart.svelte.ts
// 注意:文件扩展名是 .svelte.ts
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
function createCart() {
let items = $state<CartItem[]>([]);
let total = $derived(
items.reduce((sum, item) => sum + item.price * item.quantity, 0)
);
function addItem(item: CartItem) {
const existing = items.find(i => i.id === item.id);
if (existing) {
existing.quantity++;
} else {
items.push({ ...item, quantity: 1 });
}
}
function removeItem(id: number) {
const i = items.findIndex(item => item.id === id);
if (i !== -1) items.splice(i, 1);
}
return {
get items() { return items; },
get total() { return total; },
addItem,
removeItem
};
}
// 导出单例 store
export const cart = createCart();
文件命名约定
包含 Runes 的普通 TypeScript 文件需要命名为 .svelte.ts 或 .svelte.js,这样 Svelte 编译器才能识别其中的 $state、$derived 等 Runes。普通 .ts 文件中不能使用 Runes。
$effect 的进阶用法
$effect.pre()
在 DOM 更新之前执行的 effect,用于读取更新前的 DOM 状态(如滚动位置)。普通
$effect 在 DOM 更新之后执行。$effect.tracking()
返回布尔值,表示当前代码是否在响应式追踪上下文中运行(即是否在 $effect 或 $derived 内部)。用于编写可在追踪/非追踪上下文中都能运行的工具函数。
清理函数(Cleanup)
$effect 的回调函数可以返回一个清理函数。Svelte 会在 effect 重新运行之前、以及组件销毁时执行该清理函数。用于清除定时器、取消网络请求、取消订阅事件等。
$effect.root()
创建一个不绑定到组件生命周期的独立响应式根。在组件外(如 Node.js 服务端代码或测试中)创建响应式计算时使用,需要手动调用返回的清理函数。
<script lang="ts">
let items = $state<string[]>([]);
let listEl = $state<HTMLElement>();
// $effect.pre:DOM 更新前执行,用于保存滚动位置
$effect.pre(() => {
if (!listEl) return;
// 在 items 变化、DOM 更新前保存当前滚动位置
const savedScrollTop = listEl.scrollTop;
return () => {
// 清理函数(在此场景:DOM 更新后恢复滚动)
listEl.scrollTop = savedScrollTop;
};
});
// 带清理函数的 effect:订阅 WebSocket 消息
$effect(() => {
const ws = new WebSocket('wss://api.example.com');
ws.addEventListener('message', (e) => {
items = [...items, e.data];
});
// Svelte 在组件销毁时自动调用此清理函数
return () => ws.close();
});
// 带清理函数:订阅自定义事件总线
$effect(() => {
function handleThemeChange(e: CustomEvent) {
document.body.setAttribute('data-theme', e.detail.theme);
}
document.addEventListener('theme-changed', handleThemeChange as EventListener);
return () => {
document.removeEventListener('theme-changed', handleThemeChange as EventListener);
};
});
</script>
<ul bind:this={listEl}>
{#each items as item}<li>{item}</li>{/each}
</ul>
$effect 的常见误用
- 不要在 $effect 中修改它所追踪的状态:会导致无限循环(effect → 修改状态 → 触发 effect)。如果必须这样做,使用
untrack()函数包裹读取操作以阻止追踪。 - 不要用 $effect 替代 $derived:如果只是计算一个派生值,用
$derived更简洁,性能也更好($derived 是同步计算,$effect 是异步批处理)。 - $effect 在服务端渲染(SSR)中不执行:需要在服务端运行的逻辑不能放在 $effect 中,应该放在 load 函数或 +server.ts 中。
$state 的深层响应式原理
理解 Svelte 5 响应式系统的工作原理,有助于避免常见陷阱:
Svelte 5 响应式系统(细粒度信号)
$state(value)
┌─────────────────────────────────────────────────┐
│ 创建响应式信号(Signal) │
│ ┌────────────┐ ┌────────────────────────┐ │
│ │ 读取信号 │────▶│ 追踪为依赖(当在 │ │
│ │ signal.get │ │ $derived/$effect 中时) │ │
│ └────────────┘ └────────────────────────┘ │
│ ┌────────────┐ ┌────────────────────────┐ │
│ │ 写入信号 │────▶│ 通知所有依赖它的计算 │ │
│ │ signal.set │ │ 重新执行(细粒度更新) │ │
│ └────────────┘ └────────────────────────┘ │
└─────────────────────────────────────────────────┘
细粒度 vs 粗粒度:
- React:组件级重渲染(整个组件函数重新执行)
- Svelte 5:信号级更新(只有依赖该信号的计算重新运行)
→ Svelte 5 性能更好,无需 useMemo/useCallback 优化
<script lang="ts">
// $state 对对象/数组使用 Proxy 实现深层响应
let user = $state({
name: 'Alice',
address: {
city: '上海',
zip: '200001'
},
hobbies: ['编程', '阅读']
});
// 直接修改深层属性 ✓(Proxy 会捕获)
user.address.city = '北京';
user.hobbies.push('旅行'); // 数组变异方法也会触发响应
// 替换整个对象 ✓
user.address = { city: '广州', zip: '510001' };
// ============================
// $state.raw:不跟踪深层变化(性能优化)
// 适合大型不可变数据(如图表数据集)
let chartData = $state.raw([] as number[]);
// chartData.push(1) ← 不会触发响应(不被追踪)
chartData = [...chartData, 42]; // 必须整体替换才触发
// ============================
// $state.snapshot:获取当前值的静态快照(非响应式副本)
// 适合传递给非 Svelte 代码(如发送到 API)
async function saveUser() {
const snapshot = $state.snapshot(user); // 普通对象,非 Proxy
await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(snapshot) // Proxy 对象 JSON.stringify 可能有问题
});
}
</script>
$derived 复杂用法
<script lang="ts">
let products = $state([
{ id: 1, name: 'MacBook', price: 12999, category: '电脑' },
{ id: 2, name: 'iPhone', price: 7999, category: '手机' },
{ id: 3, name: 'iPad', price: 4999, category: '平板' }
]);
let searchTerm = $state('');
let sortBy = $state<'name' | 'price'>('name');
let minPrice = $state(0);
// 链式 $derived:逐步过滤和排序
let filtered = $derived(
products.filter(p =>
p.name.toLowerCase().includes(searchTerm.toLowerCase()) &&
p.price >= minPrice
)
);
let sorted = $derived(
[...filtered].sort((a, b) =>
sortBy === 'price' ? a.price - b.price : a.name.localeCompare(b.name)
)
);
// $derived.by:处理复杂多步计算(使用函数体而非表达式)
let stats = $derived.by(() => {
if (filtered.length === 0) return { avg: 0, max: 0, min: 0 };
const prices = filtered.map(p => p.price);
return {
avg: Math.round(prices.reduce((a, b) => a + b, 0) / prices.length),
max: Math.max(...prices),
min: Math.min(...prices)
};
});
</script>
<input bind:value={searchTerm} placeholder="搜索产品" />
<input type="number" bind:value={minPrice} placeholder="最低价格" />
<select bind:value={sortBy}>
<option value="name">按名称排序</option>
<option value="price">按价格排序</option>
</select>
<p>平均价格:¥{stats.avg},最高:¥{stats.max},最低:¥{stats.min}</p>
{#each sorted as product}
<div>{product.name} - ¥{product.price}</div>
{/each}
本章小结
本章核心要点
- $state() 创建响应式状态:对对象/数组使用 Proxy 实现深层响应,可以直接修改属性和调用数组变异方法;
$state.raw()用于大型不可变数据;$state.snapshot()获取普通对象副本。 - $derived() 创建派生状态:自动追踪依赖的响应式变量,无需手动声明;
$derived.by()用于需要多行逻辑的复杂计算;链式 $derived 可以分解复杂数据处理管道。 - $effect() 执行副作用:自动追踪依赖并在变化时重新执行;返回清理函数用于取消订阅、清除定时器;
$effect.pre()在 DOM 更新前执行;不要在 effect 中修改它所追踪的状态(会导致无限循环)。 - Runes 可跨文件使用:将响应式逻辑提取到
.svelte.ts文件中,实现多组件共享状态(替代 Svelte 4 的 store);使用 getter 函数(get items())确保从对象读取时仍保持响应式追踪。 - 细粒度响应式性能更好:Svelte 5 只更新依赖变化信号的计算,不需要像 React 那样手动使用 useMemo/useCallback 优化。