Appearance
第8.3节:Pinia - 新一代状态管理库的核心设计思想
引言
Pinia 是 Vue.js 的官方状态管理库,被设计为 Vuex 的继任者。它不仅提供了更简洁的 API,还具备完整的 TypeScript 支持、优秀的开发者体验和强大的插件系统。本节将深入分析 Pinia 的核心设计思想和实现原理。
1. Store 设计:defineStore 的实现机制
1.1 defineStore 函数的设计哲学
Pinia 的核心是 defineStore 函数,它支持两种定义方式:Options API 和 Composition API。
typescript
// Options API 风格
const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++
}
}
})
// Composition API 风格
const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
const increment = () => count.value++
return { count, doubleCount, increment }
})1.2 defineStore 的实现原理
从源码分析可以看出,defineStore 的实现包含以下关键步骤:
typescript
export function defineStore(
id: any,
setup?: any,
setupOptions?: any
): StoreDefinition {
let options: DefineStoreOptions | DefineSetupStoreOptions
const isSetupStore = typeof setup === 'function'
options = isSetupStore ? setupOptions : setup
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)
}
}
return pinia._s.get(id)!
}
return useStore
}1.3 Store 创建的两种模式
Options Store 创建
typescript
function createOptionsStore<Id, S, G, A>(
id: Id,
options: DefineStoreOptions<Id, S, G, A>,
pinia: Pinia,
hot?: boolean
): Store<Id, S, G, A> {
const { state, actions, getters } = options
const initialState: StateTree | undefined = pinia.state.value[id]
function setup() {
if (!initialState && (!__DEV__ || !hot)) {
pinia.state.value[id] = state ? state() : {}
}
const localState = toRefs(pinia.state.value[id])
return assign(
localState,
actions,
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>
)
)
}
return createSetupStore(id, setup, options, pinia, hot, true)
}Setup Store 创建
typescript
function createSetupStore<Id, SS, S, G, A>(
$id: Id,
setup: (helpers: SetupStoreHelpers) => SS,
options: DefineSetupStoreOptions<Id, S, G, A> = {},
pinia: Pinia,
hot?: boolean,
isOptionsStore?: boolean
): Store<Id, S, G, A> {
let scope!: EffectScope
// 创建 Store 的核心方法
const $patch = (partialStateOrMutator) => {
// 状态更新逻辑
}
const $reset = isOptionsStore
? function $reset() {
const { state } = options as DefineStoreOptions<Id, S, G, A>
const newState = state ? state() : {}
this.$patch(($state) => {
assign($state, newState)
})
}
: noop
const partialStore = {
_p: pinia,
$id,
$onAction: addSubscription.bind(null, actionSubscriptions),
$patch,
$reset,
$subscribe(callback, options = {}) {
// 订阅状态变化
},
$dispose() {
scope.stop()
subscriptions.clear()
actionSubscriptions.clear()
pinia._s.delete($id)
},
}
const store: Store<Id, S, G, A> = reactive(partialStore)
pinia._s.set($id, store as Store)
// 执行 setup 函数
const setupStore = runWithContext(() =>
pinia._e.run(() => (scope = effectScope()).run(() => setup({ action }))!)
)!
// 处理 actions 的包装
for (const key in setupStore) {
const prop = setupStore[key]
if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
// 状态属性
} else if (typeof prop === 'function') {
// Action 包装
const actionValue = __DEV__ && hot ? prop : wrapAction(key, prop)
setupStore[key] = actionValue
}
}
assign(store, setupStore)
assign(toRaw(store), setupStore)
return store
}2. 状态管理:state、getters、actions 的实现原理
2.1 响应式状态管理
Pinia 基于 Vue 3 的响应式系统构建,所有状态都是响应式的:
typescript
// 状态的响应式处理
const localState = toRefs(pinia.state.value[id])
// 全局状态存储
export function createPinia(): Pinia {
const scope = effectScope(true)
const state = scope.run<Ref<Record<string, StateTree>>>(() =>
ref<Record<string, StateTree>>({})
)!
const pinia: Pinia = markRaw({
install(app: App) {
setActivePinia(pinia)
pinia._a = app
app.provide(piniaSymbol, pinia)
app.config.globalProperties.$pinia = pinia
},
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
}2.2 Getters 的计算属性实现
Getters 被实现为 Vue 的计算属性,具有缓存和依赖追踪能力:
typescript
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>
)2.3 Actions 的包装和追踪
Actions 被包装以支持 $onAction 钩子和错误处理:
typescript
const action = <Fn extends _Method>(fn: Fn, name: string = ''): Fn => {
const wrappedAction = function (this: any) {
setActivePinia(pinia)
const args = Array.from(arguments)
const afterCallbackSet: Set<(resolvedReturn: any) => any> = new Set()
const onErrorCallbackSet: Set<(error: unknown) => unknown> = new Set()
function after(callback: _SetType<typeof afterCallbackSet>) {
afterCallbackSet.add(callback)
}
function onError(callback: _SetType<typeof onErrorCallbackSet>) {
onErrorCallbackSet.add(callback)
}
// 触发 action 订阅
triggerSubscriptions(actionSubscriptions, {
args,
name: wrappedAction[ACTION_NAME],
store,
after,
onError,
})
let ret: unknown
try {
ret = fn.apply(this && this.$id === $id ? this : store, args)
} catch (error) {
triggerSubscriptions(onErrorCallbackSet, error)
throw error
}
if (ret instanceof Promise) {
return ret
.then((value) => {
triggerSubscriptions(afterCallbackSet, value)
return value
})
.catch((error) => {
triggerSubscriptions(onErrorCallbackSet, error)
return Promise.reject(error)
})
}
triggerSubscriptions(afterCallbackSet, ret)
return ret
} as MarkedAction<Fn>
wrappedAction[ACTION_MARKER] = true
wrappedAction[ACTION_NAME] = name
return wrappedAction
}2.4 状态变更的 $patch 机制
typescript
function $patch(
partialStateOrMutator:
| _DeepPartial<UnwrapRef<S>>
| ((state: UnwrapRef<S>) => void)
): void {
let subscriptionMutation: SubscriptionCallbackMutation<S>
isListening = isSyncListening = false
if (typeof partialStateOrMutator === 'function') {
partialStateOrMutator(pinia.state.value[$id] as UnwrapRef<S>)
subscriptionMutation = {
type: MutationType.patchFunction,
storeId: $id,
events: debuggerEvents as DebuggerEvent[],
}
} else {
mergeReactiveObjects(pinia.state.value[$id], partialStateOrMutator)
subscriptionMutation = {
type: MutationType.patchObject,
payload: partialStateOrMutator,
storeId: $id,
events: debuggerEvents as DebuggerEvent[],
}
}
const myListenerId = (activeListener = Symbol())
nextTick().then(() => {
if (activeListener === myListenerId) {
isListening = true
}
})
isSyncListening = true
// 触发订阅回调
triggerSubscriptions(
subscriptions,
subscriptionMutation,
pinia.state.value[$id] as UnwrapRef<S>
)
}3. 插件系统:强大的扩展能力
3.1 插件接口设计
Pinia 的插件系统设计简洁而强大:
typescript
export interface PiniaPlugin {
(
context: PiniaPluginContext
): Partial<PiniaCustomProperties & PiniaCustomStateProperties> | void
}
export interface PiniaPluginContext<
Id extends string = string,
S extends StateTree = StateTree,
G = _GettersTree<S>,
A = _ActionsTree,
> {
pinia: Pinia
app: App
store: Store<Id, S, G, A>
options: DefineStoreOptionsInPlugin<Id, S, G, A>
}3.2 插件的应用机制
typescript
// 在 createSetupStore 中应用插件
pinia._p.forEach((extender) => {
if (__USE_DEVTOOLS__ && IS_CLIENT) {
const extensions = scope.run(() =>
extender({
store: store as Store,
app: pinia._a,
pinia,
options: optionsForPlugin,
})
)!
Object.keys(extensions || {}).forEach((key) =>
store._customProperties.add(key)
)
assign(store, extensions)
} else {
assign(
store,
scope.run(() =>
extender({
store: store as Store,
app: pinia._a,
pinia,
options: optionsForPlugin,
})
)!
)
}
})3.3 常见插件示例
持久化插件
typescript
function createPersistedState(options = {}) {
return (context) => {
const { store } = context
// 从 localStorage 恢复状态
const stored = localStorage.getItem(`pinia-${store.$id}`)
if (stored) {
store.$patch(JSON.parse(stored))
}
// 监听状态变化并持久化
store.$subscribe((mutation, state) => {
localStorage.setItem(`pinia-${store.$id}`, JSON.stringify(state))
})
}
}
// 使用插件
const pinia = createPinia()
pinia.use(createPersistedState())日志插件
typescript
function createLogger() {
return (context) => {
const { store } = context
store.$onAction(({ name, args, after, onError }) => {
console.log(`🚀 Action "${name}" triggered with args:`, args)
after((result) => {
console.log(`✅ Action "${name}" finished with result:`, result)
})
onError((error) => {
console.error(`❌ Action "${name}" failed with error:`, error)
})
})
store.$subscribe((mutation, state) => {
console.log(`📝 State changed:`, mutation)
})
}
}4. DevTools 集成:优秀的开发体验
4.1 DevTools 插件的注册
typescript
export function registerPiniaDevtools(app: App, pinia: Pinia) {
setupDevtoolsPlugin(
{
id: 'dev.esm.pinia',
label: 'Pinia 🍍',
logo: 'https://pinia.vuejs.org/logo.svg',
packageName: 'pinia',
homepage: 'https://pinia.vuejs.org',
componentStateTypes,
app,
},
(api) => {
// 添加时间线层
api.addTimelineLayer({
id: MUTATIONS_LAYER_ID,
label: `Pinia 🍍`,
color: 0xe5df88,
})
// 添加检查器
api.addInspector({
id: INSPECTOR_ID,
label: 'Pinia 🍍',
icon: 'storage',
treeFilterPlaceholder: 'Search stores',
actions: [
{
icon: 'content_copy',
action: () => {
actionGlobalCopyState(pinia)
},
tooltip: 'Serialize and copy the state',
},
// 更多操作...
],
})
}
)
}4.2 状态检查和时间旅行
typescript
// 格式化 Store 状态用于检查器
export function formatStoreForInspectorState(
store: StoreGeneric
): InspectorState {
const storeState = toRaw(store)
const state: InspectorState = {}
for (const key in storeState) {
const value = storeState[key]
if (typeof value === 'function') continue
state[key] = {
editable: true,
value: isRef(value) ? value.value : value,
}
}
return state
}
// 时间旅行功能
api.on.editInspectorState((payload) => {
if (payload.inspectorId === INSPECTOR_ID) {
const store = pinia._s.get(payload.nodeId)
if (store) {
const { path } = payload
const value = payload.state.value
if (path.length === 1) {
store.$patch({ [path[0]]: value })
} else {
// 深层路径更新
const obj = path.slice(0, -1).reduce((obj, key) => obj[key], store)
obj[path[path.length - 1]] = value
}
}
}
})5. TypeScript 支持:完整的类型推导
5.1 Store 类型定义
typescript
export type Store<
Id extends string = string,
S extends StateTree = {},
G = {},
A = {},
> = _StoreWithState<Id, S, G, A> &
UnwrapRef<S> &
_StoreWithGetters<G> &
(_ActionsTree extends A ? {} : A) &
PiniaCustomProperties<Id, S, G, A> &
PiniaCustomStateProperties<S>
// Store 状态类型
export type _StoreWithState<
Id extends string,
S extends StateTree,
G,
A
> = StoreProperties<Id> & {
$state: UnwrapRef<S> & PiniaCustomStateProperties<S>
$patch(stateMutation: (state: UnwrapRef<S>) => void): void
$patch(partialState: _DeepPartial<UnwrapRef<S>>): void
$reset(): void
$subscribe(
callback: SubscriptionCallback<S>,
options?: { detached?: boolean } & WatchOptions
): () => void
$onAction(
callback: StoreOnActionListener<Id, S, G, A>,
detached?: boolean
): () => void
$dispose(): void
}5.2 类型推导的实现
typescript
// 从 Setup Store 中提取状态类型
export type _ExtractStateFromSetupStore<SS> = SS extends undefined | void
? {}
: _ExtractStateFromSetupStore_Keys<SS> extends keyof SS
? _UnwrapAll<Pick<SS, _ExtractStateFromSetupStore_Keys<SS>>>
: never
// 提取状态键
export type _ExtractStateFromSetupStore_Keys<SS> = keyof {
[K in keyof SS as SS[K] extends _Method | ComputedRef ? never : K]: any
}
// 从 Setup Store 中提取 Getters 类型
export type _ExtractGettersFromSetupStore<SS> = SS extends undefined | void
? {}
: _ExtractGettersFromSetupStore_Keys<SS> extends keyof SS
? Pick<SS, _ExtractGettersFromSetupStore_Keys<SS>>
: never
// 提取 Getters 键
export type _ExtractGettersFromSetupStore_Keys<SS> = keyof {
[K in keyof SS as SS[K] extends ComputedRef ? K : never]: any
}
// 从 Setup Store 中提取 Actions 类型
export type _ExtractActionsFromSetupStore<SS> = SS extends undefined | void
? {}
: _ExtractActionsFromSetupStore_Keys<SS> extends keyof SS
? Pick<SS, _ExtractActionsFromSetupStore_Keys<SS>>
: never
// 提取 Actions 键
export type _ExtractActionsFromSetupStore_Keys<SS> = keyof {
[K in keyof SS as SS[K] extends _Method ? K : never]: any
}5.3 storeToRefs 的类型安全
typescript
export function storeToRefs<SS extends StoreGeneric>(
store: SS
): StoreToRefs<SS> {
// 在开发环境中检查是否为 Store 实例
if (__DEV__ && !isStore(store)) {
throw new Error(
`"storeToRefs()" is only usable on stores, not on plain objects. Received "${typeof store}".`
)
}
const refs = {} as StoreToRefs<SS>
for (const key in store) {
const value = store[key]
if (isRef(value) || isReactive(value)) {
refs[key as keyof StoreToRefs<SS>] = toRef(store, key)
}
}
return refs
}
// StoreToRefs 类型定义
export type StoreToRefs<SS extends StoreGeneric> = ToRefs<
StoreState<SS> & StoreGetters<SS> & PiniaCustomStateProperties<StoreState<SS>>
>6. 核心特性深度解析
6.1 模块化设计
Pinia 支持多个独立的 Store,每个 Store 都有自己的命名空间:
typescript
// 用户 Store
const useUserStore = defineStore('user', {
state: () => ({
id: null,
name: '',
email: ''
}),
actions: {
async fetchUser(id) {
const response = await api.getUser(id)
this.$patch(response.data)
}
}
})
// 购物车 Store
const useCartStore = defineStore('cart', {
state: () => ({
items: [],
total: 0
}),
getters: {
itemCount: (state) => state.items.length,
totalPrice: (state) => state.items.reduce((sum, item) => sum + item.price, 0)
},
actions: {
addItem(item) {
this.items.push(item)
this.updateTotal()
},
updateTotal() {
this.total = this.totalPrice
}
}
})
// Store 之间的组合使用
const useOrderStore = defineStore('order', () => {
const userStore = useUserStore()
const cartStore = useCartStore()
const createOrder = async () => {
if (!userStore.id) {
throw new Error('User not logged in')
}
const orderData = {
userId: userStore.id,
items: cartStore.items,
total: cartStore.total
}
const response = await api.createOrder(orderData)
cartStore.$reset() // 清空购物车
return response.data
}
return { createOrder }
})6.2 SSR 支持
Pinia 提供了完整的服务端渲染支持:
typescript
// 服务端
import { createPinia } from 'pinia'
import { createSSRApp } from 'vue'
export function createApp() {
const app = createSSRApp(App)
const pinia = createPinia()
app.use(pinia)
return { app, pinia }
}
// 在服务端预取数据
export async function renderPage(url) {
const { app, pinia } = createApp()
// 预取数据
const userStore = useUserStore(pinia)
await userStore.fetchUser()
const html = await renderToString(app)
const state = JSON.stringify(pinia.state.value)
return { html, state }
}
// 客户端水合
const { app, pinia } = createApp()
// 恢复服务端状态
if (window.__PINIA_STATE__) {
pinia.state.value = window.__PINIA_STATE__
}
app.mount('#app')6.3 热模块替换 (HMR)
typescript
// 支持热更新的 Store 定义
const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
actions: {
increment() {
this.count++
}
}
})
// 启用热更新
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useCounterStore, import.meta.hot))
}
// acceptHMRUpdate 的实现
export function acceptHMRUpdate(
initialUseStore: StoreDefinition,
hot: any
) {
return (newModule: any) => {
const pinia = getActivePinia()
if (!pinia) return
const id = initialUseStore.$id
const oldStore = pinia._s.get(id)
if (!oldStore) return
const newStore = newModule[initialUseStore.name] || newModule.default
if (!newStore) return
// 保存当前状态
const oldState = oldStore.$state
// 创建新的 Store
newStore(pinia, oldStore)
// 恢复状态
oldStore.$patch(oldState)
}
}7. 性能优化策略
7.1 按需响应式
Pinia 只对实际使用的状态创建响应式引用:
typescript
// 使用 storeToRefs 避免失去响应性
const store = useCounterStore()
const { count, doubleCount } = storeToRefs(store)
const { increment } = store // actions 不需要 toRefs
// 错误用法 - 会失去响应性
const { count, doubleCount, increment } = store7.2 计算属性缓存
Getters 基于 Vue 的计算属性实现,具有自动缓存:
typescript
const useExpensiveStore = defineStore('expensive', {
state: () => ({
items: []
}),
getters: {
// 只有当 items 变化时才重新计算
expensiveComputation: (state) => {
console.log('Computing...')
return state.items.reduce((sum, item) => {
return sum + complexCalculation(item)
}, 0)
}
}
})7.3 批量更新
使用 $patch 进行批量状态更新:
typescript
const store = useUserStore()
// 低效 - 触发多次响应式更新
store.name = 'John'
store.email = 'john@example.com'
store.age = 30
// 高效 - 批量更新
store.$patch({
name: 'John',
email: 'john@example.com',
age: 30
})
// 或使用函数形式
store.$patch((state) => {
state.name = 'John'
state.email = 'john@example.com'
state.age = 30
})8. 最佳实践
8.1 Store 组织结构
typescript
// stores/index.ts
export { useUserStore } from './user'
export { useCartStore } from './cart'
export { useOrderStore } from './order'
// stores/user.ts
export const useUserStore = defineStore('user', () => {
// 状态
const user = ref(null)
const isLoggedIn = computed(() => !!user.value)
// Actions
const login = async (credentials) => {
const response = await authAPI.login(credentials)
user.value = response.data.user
return response
}
const logout = () => {
user.value = null
// 清理其他相关状态
}
return {
// 状态
user: readonly(user),
isLoggedIn,
// Actions
login,
logout
}
})8.2 错误处理
typescript
const useApiStore = defineStore('api', () => {
const loading = ref(false)
const error = ref(null)
const fetchData = async (url) => {
loading.value = true
error.value = null
try {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
return await response.json()
} catch (err) {
error.value = err.message
throw err
} finally {
loading.value = false
}
}
return {
loading: readonly(loading),
error: readonly(error),
fetchData
}
})8.3 测试策略
typescript
// 测试 Store
import { createPinia, setActivePinia } from 'pinia'
import { useCounterStore } from '@/stores/counter'
describe('Counter Store', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('increments count', () => {
const store = useCounterStore()
expect(store.count).toBe(0)
store.increment()
expect(store.count).toBe(1)
})
it('resets count', () => {
const store = useCounterStore()
store.count = 10
store.$reset()
expect(store.count).toBe(0)
})
})总结
Pinia 作为新一代状态管理库,在设计上体现了以下核心思想:
- 简洁性:API 设计简洁直观,学习成本低
- 类型安全:完整的 TypeScript 支持,提供优秀的开发体验
- 模块化:支持多个独立 Store,便于大型应用的状态管理
- 可扩展性:强大的插件系统,支持各种扩展需求
- 开发友好:优秀的 DevTools 集成和调试支持
- 性能优化:基于 Vue 3 响应式系统,具有良好的性能表现
通过深入理解 Pinia 的实现原理,我们可以更好地利用其特性构建高质量的 Vue.js 应用,同时也为我们设计自己的状态管理方案提供了宝贵的参考。
