Svelte 5 中的 Stores 深入解析:Store 契约、svelte/store 实现原理与实战指南
【免费下载链接】svelteweb development for the rest of us项目地址: https://gitcode.com/GitHub_Trending/sv/svelte
本篇基于 Svelte 仓库官方文档 documentation/docs/06-runtime/01-stores.md 展开,系统讲解 store(存储)的核心概念:如何通过$前缀在组件中自动订阅、svelte/store模块提供的writable/readable/derived/readonly/get各方法及其完整用法、自定义 store 必须满足的 store 契约,并结合 svelte/store 共享实现 的源码剖析订阅去重、批处理队列与derived位掩码机制的底层原理,帮助你在 Svelte 5 的 runes 时代准确判断何时该用 store、如何写出符合契约的自定义 store。
什么是 Store:$前缀的自动订阅机制
一个store是通过简单的store 契约(store contract)允许对值进行响应式访问的对象。svelte/store模块 提供了满足该契约的最小 store 实现。
Store 最关键的开发者体验特性是$前缀自动订阅:
- 只要你在组件中持有一个 store 的引用,就可以在其前面加
$字符直接访问它的当前值。Svelte 编译器会替你声明这个带前缀的变量,在组件初始化时订阅该 store,并在组件销毁(或不再使用)时自动取消订阅。 - 对
$前缀变量赋值,要求该变量是一个writable store,赋值会被编译器转换为对 store.set方法的调用。 - 注意:store 必须声明在组件的顶层——不能放在
if块或函数内部。 - 局部变量(不代表 store 值的变量)不得使用
$前缀,避免与自动订阅语法冲突。
官方文档给出的最小示例:
<script> import { writable } from 'svelte/store'; const count = writable(0); console.log($count); // logs 0 count.set(1); console.log($count); // logs 1 $count = 2; console.log($count); // logs 2 </script>这段代码演示了三种访问/修改方式:读取$count、通过count.set(1)更新、直接给$count赋值(即隐式.set)。
何时使用 Stores(Svelte 5 时代的定位)
这是官方文档中非常重要、但在许多旧资料中缺失的指南。在 Svelte 5 之前,stores 是创建跨组件响应式状态或抽取逻辑的默认方案;引入 runes 之后,这些用例已经大大减少:
- 抽取逻辑:更好利用 runes 的通用响应性——runes 可以脱离组件顶层使用,甚至可以放进普通 JavaScript/TypeScript 文件(使用
.svelte.js或.svelte.ts文件扩展名)。 - 共享状态:可以直接创建一个包含所需字段的
$state对象,然后直接操作该状态。
/// file: state.svelte.js export const userState = $state({ name: 'name', /* ... */ });<!--- file: App.svelte ---> <script> import { userState } from './state.svelte.js'; </script> <p>User name: {userState.name}</p> <button onclick={() => { userState.name = 'new name'; }}> change name </button>因此,stores 仍然适用的场景收窄为:复杂的异步数据流,或者需要更手动地控制值的更新/变更监听时。如果你熟悉 RxJS 并希望复用相关经验,store 的$前缀订阅语法同样非常顺手。
svelte/store 模块:五个核心 API
svelte/store提供了一个最小的 store 实现,覆盖三类创建方式:可从外部更新的 store、只能从内部更新的 store,以及组合/派生 store 的工具函数。
writable:可外部写入的 store
writable(value, start?)创建一个值可以从组件外部设置的 store,返回带set与update方法的对象:
set(value):接收一个参数,即要设置的值。只有当 store 当前值与传入值不相等(源码中为safe_not_equal比较)时才更新并通知订阅者。update(fn):接收一个回调,回调以现有 store 值为参数,返回新值写入 store。
/// file: store.js import { writable } from 'svelte/store'; const count = writable(0); count.subscribe((value) => { console.log(value); }); // logs '0' count.set(1); // logs '1' count.update((n) => n + 1); // logs '2'第二个参数start(StartStopNotifier)——延迟/外部数据源接入的关键。如果传入函数,它会在订阅者数量从 0 变为 1时被调用(而不是 1 变 2)。该函数收到两个参数:一个set函数(设置 store 值)和一个update函数(与 store 上的update方法相同)。它必须返回一个stop函数,在订阅者数量从 1 变回 0 时被调用。
/// file: store.js import { writable } from 'svelte/store'; const count = writable(0, () => { console.log('got a subscriber'); return () => console.log('no more subscribers'); }); count.set(1); // does nothing const unsubscribe = count.subscribe((value) => { console.log(value); }); // logs 'got a subscriber', then '1' unsubscribe(); // logs 'no more subscribers'注意两个细节:
- 无任何订阅者时,
set(1)不会产生可见效果(start未运行,值更新被跳过); writable的值在 store 销毁(如页面刷新)后会丢失,若需持久化需自行实现,例如把值同步到localStorage。
源码印证:在 packages/svelte/src/store/shared/index.js 中,writable内部用Set保存订阅者,subscribers.size === 1时才调用start(set, update)并保存返回的stop;取消订阅后size === 0时调用stop()。set的通知采用subscriber_queue批处理:只有当队列为空(说明不是嵌套触发)时才 flush 队列,这使得在一次同步代码中多次set时,各订阅者回调按序集中执行,避免交错通知。
readable:只能内部更新的 store
readable创建一个值不能从外部设置的 store:第一个参数是初始值,第二个参数与writable的start参数相同——适合封装定时器、Web API 等"自驱动"的数据源。
import { readable } from 'svelte/store'; const time = readable(new Date(), (set) => { set(new Date()); const interval = setInterval(() => { set(new Date()); }, 1000); return () => clearInterval(interval); }); const ticktock = readable('tick', (set, update) => { const interval = setInterval(() => { update((sound) => (sound === 'tick' ? 'tock' : 'tick')); }, 1000); return () => clearInterval(interval); });time展示了用set直接推入最新值;ticktock展示了用update基于当前值做变换。从源码看,readable 的实现 非常薄:它直接返回writable(value, start).subscribe包装成的对象,即readable在运行时就是"剥离了set/update的 writable"。
derived:从一个或多个 store 派生
derived(stores, fn, initial_value?)从一到多个 store 派生新 store。回调在第一个订阅者订阅时运行,之后每当依赖 store 变化时重新运行。
最简形式:单个 store + 回调返回派生值:
import { type Writable } from 'svelte/store'; import { derived } from 'svelte/store'; declare const a: Writable<number>; const doubled = derived(a, ($a) => $a * 2);异步形式:回调接收第二个参数set(可选第三个参数update),可以异步调用它们设置派生值。此时还可以给derived传第三个参数作为初始值——在set/update首次被调用前,派生 store 的值就是它;若不指定,初始值为undefined。
import { derived } from 'svelte/store'; const delayed = derived( a, ($a, set) => { setTimeout(() => set($a), 1000); }, 2000 ); const delayedIncrement = derived(a, ($a, set, update) => { set($a); setTimeout(() => update((x) => x + 1), 1000); // every time $a produces a value, this produces two // values, $a immediately and then $a + 1 a second later });cleanup 形式:如果回调返回一个函数,它会在两种时机被调用:a) 回调再次运行之前;b) 最后一个订阅者取消订阅时。这是把setInterval/WebSocket 等副作用接入派生 store 的标准方式:
import { derived } from 'svelte/store'; const tick = derived( frequency, ($frequency, set) => { const interval = setInterval(() => { set(Date.now()); }, 1000 / $frequency); return () => { clearInterval(interval); }; }, 2000 );多 store 形式:两种情况下,第一个参数都可以传 store 数组代替单个 store,回调收到对应的值数组:
import { derived } from 'svelte/store'; const summed = derived([a, b], ([$a, $b]) => $a + $b); const delayed = derived([a, b], ([$a, $b], set) => { setTimeout(() => set($a + $b), 1000); });源码印证:derived 的实现 有几个值得注意的细节:
- 它通过
fn.length < 2(回调声明参数个数)判断是"自动派生"(返回值即新值)还是"手动 set"模式,而不是靠返回值是否为函数区分——所以同步模式返回普通值、异步模式返回 cleanup 函数才能被正确识别; - 每个上游 store 的回调里用位掩码
pending记录哪些依赖尚未就绪;多个依赖在同一 tick 内变化时,sync()只会在所有依赖都更新完毕后执行一次; - 回调返回的函数被保存为
cleanup,在下次sync或停止时调用,这正是文档所述"回调重跑前或最后一个订阅者离开时"的实现。
readonly:把 store 变成只读
readonly(store)返回一个新的只读 store,仍然可以订阅原始 store 的所有变化,但类型上不再暴露set:
import { readonly, writable } from 'svelte/store'; const writableStore = writable(1); const readableStore = readonly(writableStore); readableStore.subscribe(console.log); writableStore.set(2); // console: 2 // @errors: 2339 readableStore.set(2); // ERROR实现上它只是把store.subscribe重新绑定后返回一个仅含subscribe的对象,见源码。
get:不订阅时读取当前值
一般应当通过订阅 store、随时间使用其值来读取。但偶尔你需要读取一个当前并未订阅的 store 的值,get就是为此提供:
注意:
get的实现是"创建订阅 → 读取值 → 立即取消订阅",因此不建议在热点代码路径中使用。
import { get } from 'svelte/store'; const value = get(store);对应源码只有一行核心逻辑:subscribe_to_store(store, (_) => (value = _))(),即订阅回调在同步调用中捕获值,随即调用返回的 unsubscribe。
Store 契约:手写自定义 store 的规则
不依赖svelte/store,你也可以通过实现store 契约创建自己的 store:
store = { subscribe: (subscription: (value: any) => void) => (() => void), set?: (value: any) => void }- store 必须包含
.subscribe方法,接受一个订阅函数作为参数。该订阅函数必须在调用.subscribe时立即、同步地以 store 当前值被调用;之后每当 store 值变化,所有活跃订阅函数都必须被同步调用。 .subscribe必须返回一个取消订阅函数。调用它必须停止对应订阅,且对应的订阅函数不得再被 store 调用。- store可选地包含
.set方法:接受一个新值,同步调用所有活跃订阅函数。含.set的 store 称为writable store(writable store 按 Writable 接口 还应提供update)。
与 RxJS 的互操作:.subscribe也允许返回一个带.unsubscribe方法的对象(而非直接返回取消函数)。但注意:除非.subscribe同步调用了订阅函数(Observable 规范并不要求这一点),否则在 Svelte 看来 store 的值会是undefined直到它被调用。仓库中的subscribe_to_store工具函数 明确处理了这一分支:unsub.unsubscribe ? () => unsub.unsubscribe() : unsub。
源码纵深:类型定义与 runes 时代的桥接 API
类型层面:packages/svelte/src/store/public.d.ts 定义了四个核心类型——Subscriber<T>((value: T) => void)、Unsubscriber(() => void)、Updater<T>((value: T) => T)和StartStopNotifier<T>,以及Readable<T>/Writable<T>接口。Readable.subscribe的签名中带有可选的invalidate参数,这是 Svelte 内部用于让 store 主动标记依赖失效的私有通道;私有类型SubscribeInvalidateTuple把两者配对。
runes 与 store 的桥接:Svelte 5 在 svelte/store 客户端入口 额外提供了toStore与fromStore两个函数(服务端入口 index-server.js 也有简化版本):
toStore(get, set?):从一个返回 state 的函数(及可选的写入函数)创建 store。客户端实现会把get包进render_effect,使$state等 runes 值的每次变化都能推入 store——这是"从 runes 世界向 store/RxJS 世界单向输出"的官方通道。fromStore(store):把 store 转成带响应式current属性的对象(writable 可写,readable 只读)。在响应式上下文中读取current会订阅 store(内部使用 createSubscriber),在 runes 代码里就能以熟悉的属性访问方式消费 store。
这意味着:即使你按官方指南优先使用 runes,遗留的 store 生态(含社区 store 库、RxJS 兼容对象)也能通过fromStore/toStore平滑接入新的响应式体系,而无需重写。
小结:选型速查
| 需求 | 推荐方案 |
|---|---|
| 跨组件共享状态 | .svelte.js文件中的$state对象 |
| 抽取/复用响应式逻辑 | runes +.svelte.js/.svelte.ts文件 |
| 复杂异步数据流、手动控制更新与监听 | writable+start/readable |
| 组合多个 store | derived(支持同步、异步 set、cleanup、多 store 数组) |
| 对外暴露只读视图 | readonly |
| 一次性读取未订阅的 store | get(避免热点路径) |
| 对接 WebSocket/定时器等外部源 | 实现 store 契约(含 RxJS 风格的unsubscribe对象兼容) |
参考入口:概念文档 01-stores.md、实现 shared/index.js、客户端扩展 index-client.js、类型 public.d.ts、订阅桥接 utils.js 与 create-subscriber.js。
【免费下载链接】svelteweb development for the rest of us项目地址: https://gitcode.com/GitHub_Trending/sv/svelte
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考