Store(writable/readable/derived)
本教程共 50 篇 · 第 32 篇 · 更新于 2026-08-05 · 约 7 分钟阅读
本节目标:理解 Store 的概念和契约,学会使用 writable、readable、derived 三种 Store,掌握 $ 前缀自动订阅和 get() 手动读取。
Store 是什么
Store 是 Svelte 提供的一种状态管理模式。它本质上是一个对象,通过简单的”订阅-通知”机制让多个组件共享同一份数据。
打个比方,Store 就像报纸订阅服务。你(组件)订阅了某份报纸(Store),报社每次出新刊(值变化)就会送一份到你家。你不看了就退订(取消订阅),报社也不白送了。
在 Svelte 5 中,Runes($state 等)已经能解决大部分状态管理需求。但 Store 在处理异步数据流、需要精细控制订阅和通知的场景仍然有用。SvelteKit 内部也大量使用 Store。
Note在 Svelte 5 中,简单的跨组件状态共享推荐用
.svelte.js文件中的$state。Store 更适合异步数据流、RxJS 集成等复杂场景。
Store 契约
一个对象要成为 Store,只需要满足一个条件:有一个 subscribe 方法。
store = {
subscribe: (callback: (value: any) => void) => () => void,
set?: (value: any) => void
}
subscribe方法接收一个回调函数,立即用当前值调用一次,之后每次值变化时再调用。返回一个取消订阅的函数。set是可选的。有set方法的 Store 叫”可写 Store”,没有的叫”只读 Store”。
$ 前缀自动订阅
在 Svelte 组件中,你可以用 $ 前缀来访问 Store 的值。Svelte 会自动帮你订阅和取消订阅。
<script>
import { writable } from 'svelte/store';
const count = writable(0);
</script>
<!-- $count 自动订阅 count Store 的值 -->
<p>当前计数:{$count}</p>
<!-- 对 $count 赋值相当于调用 count.set() -->
<button onclick={() => $count += 1}>加一</button>
Note
$前缀只能在组件顶层使用。不能在if块或函数内部声明带$前缀的变量。局部变量(非 Store 值)不能用$前缀。
writable 可写 Store
writable 创建一个可以从外部设置的 Store。
import { writable } from 'svelte/store';
const count = writable(0);
// 订阅
const unsubscribe = count.subscribe((value) => {
console.log(value); // 立即输出 0
});
// 设置新值
count.set(1); // 输出 1
// 基于当前值更新
count.update((n) => n + 1); // 输出 2
// 取消订阅
unsubscribe();
三个方法
| 方法 | 说明 |
|---|---|
subscribe(fn) | 订阅值变化,返回取消订阅函数 |
set(value) | 直接设置新值 |
update(fn) | 接收回调,用返回值更新 Store |
启动和停止函数
writable 的第二个参数是一个函数,在第一个订阅者出现时调用,最后一个订阅者离开时清理。
import { writable } from 'svelte/store';
const count = writable(0, () => {
console.log('有人订阅了');
return () => {
console.log('没人订阅了');
};
});
// 此时还没有订阅者,启动函数不执行
count.set(1); // 什么都不输出
const unsub = count.subscribe((v) => console.log(v));
// 输出:"有人订阅了",然后输出 1
unsub();
// 输出:"没人订阅了"
Tip启动函数非常适合用来设置定时器、WebSocket 连接等——只有有人需要数据时才建立连接,没人了就断开,节省资源。
实战:带本地存储的计数器
// counter.js
import { writable } from 'svelte/store';
function createCounter() {
const stored = localStorage.getItem('count');
const { subscribe, set, update } = writable(stored ? parseInt(stored) : 0);
return {
subscribe,
increment: () => update((n) => {
const next = n + 1;
localStorage.setItem('count', next);
return next;
}),
reset: () => {
localStorage.removeItem('count');
set(0);
}
};
}
export const counter = createCounter();
<script>
import { counter } from './counter.js';
</script>
<p>计数:{$counter}</p>
<button onclick={() => counter.increment()}>加一</button>
<button onclick={() => counter.reset()}>重置</button>
readable 只读 Store
readable 创建一个只能从内部修改的 Store。外部只能订阅,不能 set 或 update。
import { readable } from 'svelte/store';
const time = readable(new Date(), (set) => {
// 启动函数:设置定时器
set(new Date());
const interval = setInterval(() => {
set(new Date());
}, 1000);
// 返回清理函数
return () => clearInterval(interval);
});
在组件中使用:
<script>
import { time } from './time.js';
</script>
<p>当前时间:{$time.toLocaleTimeString()}</p>
Note
readable的启动函数也接收set和update两个参数。你可以用update做基于当前值的更新。第一个参数是初始值,在启动函数执行前就会被使用。
实战:tick-tock Store
import { readable } from 'svelte/store';
export const ticktock = readable('tick', (set, update) => {
const interval = setInterval(() => {
update((sound) => (sound === 'tick' ? 'tock' : 'tick'));
}, 1000);
return () => clearInterval(interval);
});
derived 派生 Store
derived 从一个或多个 Store 派生出新值。当依赖的 Store 变化时,派生 Store 自动更新。
import { derived } from 'svelte/store';
// 假设 a 是一个 writable Store
const doubled = derived(a, ($a) => $a * 2);
多 Store 派生
传入数组可以依赖多个 Store。
import { derived } from 'svelte/store';
const summed = derived([a, b], ([$a, $b]) => $a + $b);
异步派生
回调可以接收 set 参数,实现异步派生。
const delayed = derived(
a,
($a, set) => {
setTimeout(() => set($a * 2), 1000);
},
0 // 初始值,在 set 第一次被调用前使用
);
Note异步派生必须提供第三个参数作为初始值,否则在
set被调用前 Store 值为undefined。
清理函数
如果派生回调返回一个函数,它会在下次回调执行或最后一个订阅者离开时调用。
const tick = derived(
frequency,
($frequency, set) => {
const interval = setInterval(() => {
set(Date.now());
}, 1000 / $frequency);
return () => clearInterval(interval);
},
0
);
get() 手动读取
有时候你不在组件内部,不能用 $ 前缀,但需要读取 Store 的当前值。用 get() 函数。
import { get } from 'svelte/store';
import { count } from './counter.js';
const value = get(count);
console.log(value);
Note
get()的原理是订阅、读取值、立即取消订阅。它不适合在频繁调用的代码路径中使用,性能不理想。
readonly 只读包装
readonly 把一个可写 Store 包装成只读 Store。
import { readonly, writable } from 'svelte/store';
const writableStore = writable(1);
const readableStore = readonly(writableStore);
readableStore.subscribe(console.log); // 输出 1
writableStore.set(2); // 输出 2
readableStore.set(2); // 报错:readonly 没有 set 方法
Store 契约实战:自定义 Store
因为 Store 只要满足”有 subscribe 方法”就行,你可以自己实现。
// 自定义 Store:值翻倍后才通知订阅者
function createEvenStore(initial) {
let value = initial;
const subscribers = new Set();
return {
subscribe(callback) {
callback(value);
subscribers.add(callback);
return () => subscribers.delete(callback);
},
set(newValue) {
value = newValue;
subscribers.forEach((cb) => cb(value));
},
get() {
return value;
}
};
}
Tip自定义 Store 可以跟 RxJS Observable 互操作。Svelte 的
subscribe方法也兼容返回带unsubscribe方法的对象的形式。
Store vs Runes:什么时候用什么
| 场景 | 推荐方案 |
|---|---|
| 组件内状态 | $state |
| 跨组件共享简单状态 | .svelte.js 文件中的 $state |
| 异步数据流(定时器、WebSocket) | Store(readable) |
| 需要精细控制订阅时机 | Store |
| RxJS 集成 | Store(兼容 Observable) |
| 派生计算 | $derived 或 derived |
// Svelte 5 推荐的跨组件状态共享方式
// state.svelte.js
export const userState = $state({
name: '码上学',
loggedIn: false
});
<script>
import { userState } from './state.svelte.js';
</script>
<p>用户名:{userState.name}</p>
<button onclick={() => userState.name = '新名字'}>
改名
</button>
本节回顾
- Store 是通过订阅-通知机制共享数据的对象,核心是
subscribe方法 $前缀在组件中自动订阅 Store,赋值给$store等同于调用set()writable创建可写 Store,有set和update方法readable创建只读 Store,值只能从内部改变derived从其他 Store 派生新值,支持多 Store 依赖和异步派生get()手动读取 Store 当前值,但不宜频繁使用- Svelte 5 中简单状态推荐用
.svelte.js中的$state,Store 适合异步和精细控制场景