Vanilla Store 实现详解
Vanilla Store(createStore)是 Zustand 的核心:不依赖 React,用闭包 + 订阅集合实现最小状态容器。create() 只是在 Store API 外包一层 useStore Hook。
源码入口:src/vanilla.ts。
1. Store API 一览
| 方法 | 作用 |
|---|---|
getState() | 读当前状态(同步、非响应式) |
setState(partial, replace?) | 写状态并通知订阅者 |
subscribe(listener) | listener(state, prevState);返回取消函数 |
getInitialState() | 返回创建时的初始快照(SSR / 水合对齐用) |
2. createStore 心智模型
闭包变量 state
监听器集合 listeners: Set<Listener>
createState(set, get, api) → 初始化 state
setState → 合并/替换 → 若引用变则 listeners.forEach无 Redux 的 reducer 管道、无 Proxy(除非中间件加 Immer);一次 set 一次通知轮次。
3. setState 语义
3.1 两种写入形态
| 形态 | 调用 | 行为 |
|---|---|---|
| 部分更新(默认) | set({ count: 1 }) 或 set(s => ({ ... })) | 浅合并进现有 state |
| 完全替换 | set(newState, true) | 丢弃旧顶层,整体替换 |
函数式更新:set((state) => partial),与 React setState 类似,便于基于上一拍计算。
3.2 何时不通知
若合并/替换后 Object.is(nextState, prevState) 为真,则不遍历 listeners。因此返回同一引用或结构未变的更新不会触发订阅(与 React 快照短路一致)。
3.3 浅合并规则
默认对顶层 key 做 Object.assign({}, state, partial):
- 更新
{ count: 1 }不影响同层其他 key。 - 更新
{ user: newUser }会整键替换user,不会深合并user.name。
嵌套结构需:手动展开、immer 中间件,或不可变工具函数。
4. subscribe
- 使用
Set<Listener>存储,添加/删除 O(1)。 - 每次
setState成功变更后,同步调用所有 listener(无内置批量合并)。 - 返回
() => listeners.delete(listener)用于清理。
React 层的 useSyncExternalStore 会把 subscribe 与组件生命周期绑定。
5. 初始化流程
- 创建空
api(setState/getState/subscribe/getInitialState)。 - 调用用户
createState(set, get, api),返回值作为初始state。 getInitialState固定指向该初始快照。
create((set, get) => ({ ... })) 里的 get 可在 action 中读最新 state,避免闭包陈旧(仍须在事件/异步回调里调用,而非 render 期间副作用滥用)。
6. 与 React 层的关系
| Vanilla | React create |
|---|---|
createStore | create = createStore + 绑定 useStore |
| 任意环境可用 | 组件内 useStore(api, selector) |
同一 store 实例可在 React 组件外 getState / subscribe,适合与路由、Web Worker 消息桥接。
7. 阅读源码时看什么
createStoreImpl:setState合并分支与Object.is短路。replace参数与「非对象 partial」的替换路径。- 中间件如何包装
set/get(见 04-中间件系统)。
不必背诵完整实现;理解 闭包 state + Set 订阅 + 浅合并 即可推导大部分行为。
8. 总结
Vanilla Store 刻意保持极小表面:写、读、订阅、初始快照。性能与并发语义主要由 React 层的 useSyncExternalStore 与选择器策略决定(03-React集成、07-性能优化)。