Appearance
Pinia - 新一代状态管理库的核心设计思想
引言
Pinia 是 Vue 官方的状态管理库,作为 Vuex 的替代方案。它提供了更简洁的 API、完整的 TypeScript 支持和可扩展的插件系统。其设计核心是模块化和扁平化,取消了 Vuex 中的 mutations 和多层 modules。
本节将深入其源码,分析 Pinia 是如何通过 defineStore 和一个全局 ref 来实现其状态管理系统的。
1. createPinia:全局状态容器
Pinia 的工作流程始于 createPinia()。这个函数的核心职责是创建 Pinia 实例,这个实例是所有 store 的管理者。
typescript
// packages/pinia/src/createPinia.ts
export function createPinia(): Pinia {
// 1. 【核心】创建一个 EffectScope,
// 并在其内部创建一个全局的 ref 对象,用于存储“所有” store 的 state
const scope = effectScope(true)
const state = scope.run<Ref<Record<string, StateTree>>>(() =>
ref<Record<string, StateTree>>({}) // 所有的 state 都存在这里
)!
const pinia: Pinia = {
// 2. install:
// 将 pinia 实例和这个全局 state
// 通过 provide 提供给所有子组件
install(app: App) {
setActivePinia(pinia)
app.provide(piniaSymbol, pinia)
// ...
},
_e: scope, // EffectScope 实例
_s: new Map<string, StoreGeneric>(), // Store 实例的缓存 Map
state, // 全局 state = ref({})
}
return pinia
}Pinia 的第一个核心设计:Pinia 实例本身非常轻量。它只管理一个全局 ref(pinia.state)用于存放所有 state,以及一个 Map(pinia._s)用于缓存 store 实例。
2. defineStore:“Store 定义”函数
defineStore 并不创建 store 实例。它是一个“工厂”,用于定义一个 store 的“配方”(state, getters, actions),并返回一个 useStore 函数。
typescript
// packages/pinia/src/store.ts
export function defineStore(
id: string,
setup: // (Options API 对象 或 Setup 函数),
setupOptions?: any
): StoreDefinition {
const isSetupStore = typeof setup === 'function'
// defineStore 只返回一个“useStore”函数
function useStore(pinia?: Pinia | null): StoreGeneric {
// ... (具体实现在下一节)
}
return useStore
}3. useStore:实例的“获取与创建”
useStore (例如 useCounterStore()) 才是 Pinia 的执行核心。它负责获取或创建 store 实例。
typescript
// defineStore 内部返回的 useStore 函数
function useStore(pinia?: Pinia | null): StoreGeneric {
// 1. 【获取 Pinia】
// 通过 inject(piniaSymbol) 找到全局 Pinia 实例
pinia = pinia || (hasInjectionContext() ? inject(piniaSymbol) : null)
setActivePinia(pinia)
pinia = activePinia!
// 2. 【检查缓存】
// 检查 pinia._s (Map 缓存) 中是否“已经”创建过这个 store?
if (!pinia._s.has(id)) {
// 3. 【创建 Store】
// 如果缓存中没有,就创建它
if (isSetupStore) {
// 路径 A: Setup Store ( () => { ... } )
createSetupStore(id, setup, options, pinia)
} else {
// 路径 B: Options Store ( { state: ... } )
createOptionsStore(id, options as any, pinia)
}
}
// 4. 【返回实例】
// 从缓存中获取 store 实例并返回
return pinia._s.get(id)!
}Pinia 的第二个核心设计:store 是单例的。useStore() 在第一次被调用时创建 store,并将其缓存。后续所有调用始终返回同一个实例。
4. createOptionsStore vs createSetupStore:统一实现
Pinia 巧妙地将两种 API 风格统一到了一个实现上。
createOptionsStore (Options API)
createOptionsStore 只是一个“转换器”。它接收 Options API(state, getters, actions),将其转换为一个 setup 函数,然后调用 createSetupStore。
typescript
// packages/pinia/src/store.ts
function createOptionsStore(
id: string,
options: DefineStoreOptions,
pinia: Pinia,
) {
const { state, getters, actions } = options
function setup() {
// 1. 【State】
// 执行 state() 函数,将其返回值注册到“全局 state”
pinia.state.value[id] = state ? state() : {}
// 创建 state 的本地引用
const localState = toRefs(pinia.state.value[id])
// 2. 【Getters】
// 将 getters 转换为 computed
const computedGetters = Object.keys(getters || {}).reduce(
(gettersMap, name) => {
gettersMap[name] = markRaw(
computed(() => {
// ...
return getters![name].call(store, store)
})
)
return gettersMap
},
{}
)
// 3. 【返回】
// 返回“转换”后的 Setup Store 结构
return assign(
localState, // { count: Ref<0> }
actions, // { increment: Function }
computedGetters // { double: ComputedRef<0> }
)
}
// 4. 【调用】
// 最终还是调用 createSetupStore
return createSetupStore(id, setup, options, pinia)
}createSetupStore (Setup API)
createSetupStore 是真正的 store 构造器。
typescript
// packages/pinia/src/store.ts
function createSetupStore(
$id: string,
setup: Function,
options: DefineSetupStoreOptions,
pinia: Pinia,
): Store {
// 1. 【创建基础 Store】
// 创建一个响应式对象,包含 $id, $patch, $onAction 等
const store: Store = reactive(partialStore)
// 2. 【缓存 Store】
// 在执行 setup 之前,先将“空壳” store 放入缓存
// (这允许在 setup 内部调用 useStore() 引用自己)
pinia._s.set($id, store)
// 3. 【执行 Setup】
// 在 EffectScope 中执行用户传入的 setup() 函数
const setupStore = pinia._e.run(() =>
(scope = effectScope()).run(() => setup())
)!
// 4. 【分离属性】
// 遍历 setup() 的返回值
for (const key in setupStore) {
const prop = setupStore[key]
if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
// 【State】
// 如果是 ref 或 reactive,它就是“状态”
// (在 Options Store 中,这里会被注册到 pinia.state.value)
} else if (isRef(prop) && isComputed(prop)) {
// 【Getters】
// (无需特殊处理,它已经是 computed)
} else if (typeof prop === 'function') {
// 【Actions】
// 如果是函数,它就是“Action”
// 【核心】用 wrapAction 包装它
setupStore[key] = wrapAction(key, prop)
}
}
// 5. 【合并】
// 将 setup() 返回的(已包装的)属性
// 合并到基础 store 实例上
assign(store, setupStore)
return store
}5. storeToRefs:为什么需要它?
store 实例上的 state 和 getters 都是响应式的(ref / computed)。如果直接解构,会丢失响应性:
javascript
const store = useCounterStore()
// 错误!count 是一个“值”(0),不是 ref
const { count, doubleCount } = storestoreToRefs (packages/pinia/src/storeToRefs.ts) 是一个辅助函数,它只遍历 store 的 state 和 getters,并为它们创建 toRef 引用。
typescript
// packages/pinia/src/storeToRefs.ts
export function storeToRefs<SS extends StoreGeneric>(
store: SS
): StoreToRefs<SS> {
const refs = {} as StoreToRefs<SS>
for (const key in store) {
const value = store[key]
// 【关键】
// 只转换 ref, reactive 和 computed 属性
// (它会“跳过” actions 和 $patch 等函数)
if (isRef(value) || isReactive(value)) {
refs[key as keyof StoreToRefs<SS>] =
toRef(store, key) // 创建一个链接到 store 的 ref
}
}
return refs
}6. 插件与订阅 ($patch, $onAction)
Pinia 移除了 mutations,但提供了更灵活的“拦截”机制。
$patch:状态“拦截”
$patch 允许你批量修改 state。$patch 的实现就是直接修改 pinia.state.value[id] 这个全局状态对象。
typescript
// store 上的 $patch 方法 (简化)
function $patch(partialStateOrMutator) {
// 触发订阅 (before)
triggerSubscriptions(subscriptions, ...)
if (typeof partialStateOrMutator === 'function') {
// 1. $patch(state => { ... })
// 直接操作“全局 state”中的那片区域
partialStateOrMutator(pinia.state.value[$id])
} else {
// 2. $patch({ ... })
// 合并对象到“全局 state”
mergeReactiveObjects(pinia.state.value[$id], partialStateOrMutator)
}
// 触发订阅 (after)
triggerSubscriptions(subscriptions, ...)
}store.$subscribe(callback) 就是在订阅 pinia.state.value[$id] 的变更。
$onAction:Actions“拦截”
$onAction 是通过 wrapAction 实现的。createSetupStore 在遍历 setup 返回值时,会用 wrapAction 包装所有函数:
typescript
// packages/pinia/src/actions.ts (简化)
function wrapAction(name: string, action: Function): Function {
return function (this: any) {
// ...
const args = Array.from(arguments)
// 1. 【发布“before”】
// 触发 $onAction 订阅
triggerSubscriptions(actionSubscriptions, {
name,
store,
args,
after, // 允许“after”回调
onError, // 允许“onError”回调
})
let ret: unknown
try {
// 2. 【执行 Action】
ret = action.apply(this, args)
} catch (error) {
// 3. 【发布“error”】
triggerSubscriptions(onErrorCallbackSet, error)
throw error
}
if (ret instanceof Promise) {
// 4. 【处理异步 Action】
return ret
.then((value) => {
// 5. 【发布“after”】
triggerSubscriptions(afterCallbackSet, value)
return value
})
.catch(...)
}
// 6. 【发布“after”】 (同步 Action)
triggerSubscriptions(afterCallbackSet, ret)
return ret
}
}总结
Pinia 的设计基于 Vue 3 的响应式系统,而不是另建一套:
- 全局状态 (
createPinia):所有store的state共享同一个全局ref(pinia.state)。 defineStore(工厂):返回一个useStore函数。useStore(单例):负责inject全局pinia,并通过pinia._s(Map 缓存) 获取或创建store实例。- 统一实现:
createOptionsStore只是一个转换器,它将{ state, getters, actions }转换为setup函数,最终统一由createSetupStore处理。 createSetupStore(构造器):- State:注册到
pinia.state.value[id]。 - Getters:被转换为
computed。 - Actions:被
wrapAction包装,以支持onAction钩子。
- State:注册到
- 插件 (
plugin.use):在createSetupStore期间,store实例被创建后,会遍历pinia._p(插件数组) 并执行它们,允许插件向store实例上添加新属性(如$persistedState)。 - 解构:
storeToRefs是必需的,因为它为state和getters创建了toRef,保持了响应式连接。
