Appearance
第 15 章 状态管理:Pinia 内核
本章要点
- Pinia 的架构哲学:从 Vuex 的 mutation/action/getter 简化为 state/action/getter
- createPinia 的实现:一个 effectScope + 一个 reactive Map 构成的轻量容器
- defineStore 的两种风格:Options Store 与 Setup Store 的内部统一
- Store 的创建与激活:延迟初始化、单例保证与 Pinia 实例绑定
- 响应式状态管理:$patch 的智能合并与批量更新优化
- Store 间的交互:如何在一个 Store 中使用另一个 Store
- SSR 状态序列化:服务端如何注入初始状态到客户端
- Pinia 插件系统:扩展每个 Store 的能力
如果说依赖注入是 Vue 生态的"神经网络",那么 Pinia 就是建立在这个网络之上的"大脑"。作为 Vue 官方推荐的状态管理库,Pinia 用不到 2000 行核心代码实现了一个类型安全、支持 SSR、可扩展的全局状态方案。
Vuex 曾是 Vue 2 时代的标配,但它的 mutation 机制饱受争议——mutation 和 action 的边界模糊,mutation 必须同步的限制常常被开发者绕过。Pinia 的回答是:去掉 mutation,让 action 统一处理所有状态变更。这不是简化,而是认识到 mutation 这层抽象并没有带来足够的价值。
15.1 createPinia:状态容器的诞生
源码解析
typescript
// packages/pinia/src/createPinia.ts
export function createPinia(): Pinia {
const scope = effectScope(true)
const state = scope.run<Ref<Record<string, StateTree>>>(() =>
ref<Record<string, StateTree>>({})
)!
let _p: Pinia['_p'] = []
let toBeInstalled: PiniaPlugin[] = []
const pinia: Pinia = markRaw({
install(app: App) {
setActivePinia(pinia)
pinia._a = app
app.provide(piniaSymbol, pinia)
app.config.globalProperties.$pinia = pinia
toBeInstalled.forEach(plugin => _p.push(plugin))
toBeInstalled = []
},
use(plugin) {
if (!this._a) {
toBeInstalled.push(plugin)
} else {
_p.push(plugin)
}
return this
},
_p,
_a: null,
_e: scope,
_s: new Map<string, StoreGeneric>(),
state,
})
return pinia
}这段代码信息量极大,逐层拆解:
effectScope(true):独立的副作用作用域
typescript
const scope = effectScope(true)effectScope(true) 创建一个分离的(detached)作用域。true 参数意味着这个作用域不会被父作用域(组件的 setup)收集。为什么?因为 Pinia 的生命周期是应用级的,不应该跟随任何组件的卸载而销毁。
state:全局状态树
typescript
const state = scope.run(() => ref({}))所有 Store 的状态都存放在这个单一的 ref 中。key 是 Store 的 id,value 是该 Store 的 state。这使得:
- DevTools 集成:一个入口就能看到所有状态
- SSR 序列化:
JSON.stringify(pinia.state.value)即可导出所有状态 - 时间旅行调试:替换整个 state 就能回到任意时间点
_s:Store 实例注册表
typescript
_s: new Map<string, StoreGeneric>()所有已创建的 Store 实例都注册在这里。这保证了 Store 的单例语义——同一个 id 的 Store 只会创建一次。
markRaw 的妙用
typescript
const pinia: Pinia = markRaw({ ... })Pinia 实例被 markRaw 标记为永不响应式。为什么?因为 Pinia 对象本身不需要被追踪——它是一个容器,不是状态。如果 Pinia 对象变成响应式的,它内部的 _s(Map)、_p(插件数组)等都会被深度代理,造成不必要的性能开销。
15.2 defineStore:两种风格,一种内核
Options Store
typescript
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Counter'
}),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++
}
}
})Setup Store
typescript
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const name = ref('Counter')
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, name, doubleCount, increment }
})两种写法产生完全一样的 Store。但内部实现路径不同——Options Store 会被转换为等价的 Setup Store。
defineStore 的实现
typescript
export function defineStore(
idOrOptions: any,
setup?: any,
setupOptions?: any
): StoreDefinition {
let id: string
let options: DefineStoreOptions | DefineSetupStoreOptions
// 重载解析
const isSetupStore = typeof setup === 'function'
if (typeof idOrOptions === 'string') {
id = idOrOptions
options = isSetupStore ? setupOptions : setup
} else {
options = idOrOptions
id = idOrOptions.id
}
function useStore(pinia?: Pinia | null, hot?: StoreGeneric): StoreGeneric {
// 获取当前组件实例
const hasContext = hasInjectionContext()
pinia = pinia || (hasContext ? inject(piniaSymbol, null) : null)
if (pinia) setActivePinia(pinia)
pinia = activePinia!
// 单例检查
if (!pinia._s.has(id)) {
// 首次使用,创建 Store
if (isSetupStore) {
createSetupStore(id, setup, options, pinia)
} else {
createOptionsStore(id, options as any, pinia)
}
}
const store: StoreGeneric = pinia._s.get(id)!
return store as any
}
useStore.$id = id
return useStore as any
}注意 defineStore 返回的不是 Store 本身,而是一个 useStore 函数。Store 的创建是延迟的——只有在组件中第一次调用 useStore() 时才会真正创建。
15.3 createOptionsStore:Options 到 Setup 的转换
typescript
function createOptionsStore<Id extends string>(
id: Id,
options: DefineStoreOptions<Id, any, any, any>,
pinia: Pinia
): Store<Id> {
const { state, actions, getters } = options
const initialState: StateTree | undefined = pinia.state.value[id]
let store: Store<Id>
function setup() {
if (!initialState) {
// 首次创建
pinia.state.value[id] = state ? state() : {}
}
// 将 state 的每个属性转换为 ref(toRefs 保持响应性连接)
const localState = toRefs(pinia.state.value[id])
return Object.assign(
localState,
actions,
// 将 getters 转换为 computed
Object.keys(getters || {}).reduce((computedGetters, name) => {
computedGetters[name] = markRaw(
computed(() => {
setActivePinia(pinia)
const store = pinia._s.get(id)!
return getters![name].call(store, store)
})
)
return computedGetters
}, {} as Record<string, ComputedRef>)
)
}
store = createSetupStore(id, setup, options, pinia, true)
return store as any
}