Pinia 状态管理深度解析
Pinia是Vue3的官方推荐状态管理库,提供了更简洁的API、更好的TypeScript支持和更强的开发体验。
🎯 Pinia核心概念
mermaid
graph TB
A[Vue Components] --> B[Store Actions]
B --> C[Store State]
C --> A
D[Store Getters] --> A
C --> D
E[Plugins] --> F[Pinia Instance]
F --> G[Store Definitions]
G --> H[Store Instances]
subgraph "Pinia Store"
C
B
D
I[Subscriptions]
end
B --> I
C --> I
核心特性
- 直观的API:没有mutations,直接修改state
- TypeScript支持:完整的类型推导
- 开发工具:Vue DevTools完美集成
- 插件系统:可扩展的插件架构
- 服务端渲染:SSR友好
- 代码分割:自动代码分割支持
🏗️ Pinia源码实现原理
1. createPinia核心实现
javascript
function createPinia() {
const scope = effectScope(true)
const state = scope.run(() => ref({}))
let _p = []
let toBeInstalled = []
const pinia = markRaw({
install(app) {
setActivePinia(pinia)
if (!isVue2) {
pinia._a = app
app.provide(piniaSymbol, pinia)
app.config.globalProperties.$pinia = pinia
if (USE_DEVTOOLS) {
registerPiniaDevtools(app, pinia)
}
toBeInstalled.forEach((plugin) => _p.push(plugin))
toBeInstalled = []
}
},
use(plugin) {
if (!this._a && !isVue2) {
toBeInstalled.push(plugin)
} else {
_p.push(plugin)
}
return this
},
_p,
_a: null,
_e: scope,
_s: new Map(),
state
})
if (USE_DEVTOOLS && typeof Proxy !== 'undefined') {
pinia.use(devtoolsPlugin)
}
return pinia
}
2. defineStore实现原理
javascript
function defineStore(idOrOptions, setup, setupOptions) {
let id
let options
const isSetupStore = typeof setup === 'function'
if (typeof idOrOptions === 'string') {
id = idOrOptions
options = isSetupStore ? setupOptions : setup
} else {
options = idOrOptions
id = idOrOptions.id
}
function useStore(pinia, hot) {
const currentInstance = getCurrentInstance()
pinia = pinia || (currentInstance && inject(piniaSymbol, null))
if (pinia) setActivePinia(pinia)
pinia = activePinia
if (!pinia._s.has(id)) {
if (isSetupStore) {
createSetupStore(id, setup, options, pinia)
} else {
createOptionsStore(id, options, pinia)
}
}
const store = pinia._s.get(id)
if (hot) {
const hotId = '__hot:' + id
const newStore = isSetupStore
? createSetupStore(hotId, setup, options, pinia, true)
: createOptionsStore(hotId, assign({}, options), pinia, true)
hot._hotUpdate(newStore)
delete pinia.state.value[hotId]
pinia._s.delete(hotId)
}
if (IS_CLIENT && currentInstance && currentInstance.proxy && !hot) {
const vm = currentInstance.proxy
const cache = '_pStores' in vm ? vm._pStores : (vm._pStores = {})
cache[id] = store
}
return store
}
useStore.$id = id
return useStore
}
3. createSetupStore实现
javascript
function createSetupStore($id, setup, options = {}, pinia, hot, isOptionsStore) {
let scope
const optionsForPlugin = assign({ actions: {} }, options)
if (!pinia._e.active) {
throw new Error('Pinia destroyed')
}
const $subscriptions = []
const $actionSubscriptions = []
let isListening
let isSyncListening
let subscriptions = markRaw([])
let actionSubscriptions = markRaw([])
let debuggerEvents
const initialState = pinia.state.value[$id]
if (!isOptionsStore && !initialState && (!hot)) {
if (isVue2) {
set(pinia.state.value, $id, {})
} else {
pinia.state.value[$id] = {}
}
}
const hotState = ref({})
let activeListener
function $patch(partialStateOrMutator) {
let subscriptionMutation
isListening = isSyncListening = false
if (typeof partialStateOrMutator === 'function') {
partialStateOrMutator(pinia.state.value[$id])
subscriptionMutation = {
type: MutationType.patchFunction,
storeId: $id,
events: debuggerEvents
}
} else {
mergeReactiveObjects(pinia.state.value[$id], partialStateOrMutator)
subscriptionMutation = {
type: MutationType.patchObject,
payload: partialStateOrMutator,
storeId: $id,
events: debuggerEvents
}
}
const myListenerId = (activeListener = Symbol())
nextTick().then(() => {
if (activeListener === myListenerId) {
isListening = true
}
})
isSyncListening = true
triggerSubscriptions(subscriptions, subscriptionMutation, pinia.state.value[$id])
}
const $reset = isOptionsStore
? function $reset() {
const { state } = options
const newState = state ? state() : {}
this.$patch(($state) => {
assign($state, newState)
})
}
: noop
function $dispose() {
scope.stop()
subscriptions = []
actionSubscriptions = []
pinia._s.delete($id)
}
function wrapAction(name, action) {
return function () {
setActivePinia(pinia)
const args = Array.from(arguments)
const afterCallbackList = []
const onErrorCallbackList = []
function after(callback) {
afterCallbackList.push(callback)
}
function onError(callback) {
onErrorCallbackList.push(callback)
}
triggerSubscriptions(actionSubscriptions, {
args,
name,
store,
after,
onError
})
let ret
try {
ret = action.apply(this, args)
} catch (error) {
triggerSubscriptions(onErrorCallbackList, error)
throw error
}
if (ret instanceof Promise) {
return ret
.then((value) => {
triggerSubscriptions(afterCallbackList, value)
return value
})
.catch((error) => {
triggerSubscriptions(onErrorCallbackList, error)
return Promise.reject(error)
})
}
triggerSubscriptions(afterCallbackList, ret)
return ret
}
}
const partialStore = {
_p: pinia,
$id,
$onAction: addSubscription.bind(null, actionSubscriptions),
$patch,
$reset,
$subscribe(callback, options = {}) {
const removeSubscription = addSubscription(subscriptions, callback, options.detached, () => stopWatcher())
const stopWatcher = scope.run(() =>
watch(
() => pinia.state.value[$id],
(state) => {
if (options.flush === 'sync' ? isSyncListening : isListening) {
callback(
{
storeId: $id,
type: MutationType.direct,
events: debuggerEvents
},
state
)
}
},
assign({}, $subscribeOptions, options)
)
)
return removeSubscription
},
$dispose
}
const store = reactive(partialStore)
pinia._s.set($id, store)
const runWithContext = (pinia._a && pinia._a.runWithContext) || fallbackRunWithContext
const setupStore = runWithContext(() =>
pinia._e.run(() => (scope = effectScope()).run(setup))
)
for (const key in setupStore) {
const prop = setupStore[key]
if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
if (hot) {
set(hotState.value, key, toRef(setupStore, key))
} else if (!isOptionsStore) {
if (initialState && shouldHydrate(prop)) {
if (isRef(prop)) {
prop.value = initialState[key]
} else {
mergeReactiveObjects(prop, initialState[key])
}
}
if (isVue2) {
set(pinia.state.value[$id], key, prop)
} else {
pinia.state.value[$id][key] = prop
}
}
} else if (typeof prop === 'function') {
const actionValue = hot ? prop : wrapAction(key, prop)
if (isVue2) {
set(setupStore, key, actionValue)
} else {
setupStore[key] = actionValue
}
optionsForPlugin.actions[key] = prop
}
}
if (isVue2) {
Object.keys(setupStore).forEach((key) => {
set(store, key, setupStore[key])
})
} else {
assign(store, setupStore)
assign(toRaw(store), setupStore)
}
Object.defineProperty(store, '$state', {
get: () => (hot ? hotState.value : pinia.state.value[$id]),
set: (state) => {
if (hot) {
throw new Error('cannot set hotState')
}
$patch(($state) => {
assign($state, state)
})
}
})
if (USE_DEVTOOLS) {
store._customProperties = markRaw(new Set())
}
pinia._p.forEach((extender) => {
if (USE_DEVTOOLS) {
const extensions = scope.run(() => extender({
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,
app: pinia._a,
pinia,
options: optionsForPlugin
})))
}
})
if (store.$state && typeof store.$state === 'object' && typeof store.$state.constructor === 'function' && !store.$state.constructor.toString().includes('[native code]')) {
console.warn('The "state" must be a plain object. It cannot be\n' +
'\t- a class instance\n' +
'\t- a function\n' +
'\t- a Date\n' +
'\t- etc.\n' +
'If you need complex types, use `markRaw()` to wrap them.')
}
if (initialState && isOptionsStore && options.hydrate) {
options.hydrate(store.$state, initialState)
}
isListening = true
isSyncListening = true
return store
}
🎨 高级用法与最佳实践
1. Setup Store模式
javascript
// stores/user.js
export const useUserStore = defineStore('user', () => {
// State
const profile = ref(null)
const permissions = ref([])
const preferences = ref({})
// Getters
const isLoggedIn = computed(() => !!profile.value)
const fullName = computed(() => {
return profile.value
? `${profile.value.firstName} ${profile.value.lastName}`
: ''
})
const hasPermission = computed(() => {
return (permission) => permissions.value.includes(permission)
})
// Actions
async function login(credentials) {
try {
const response = await api.login(credentials)
const { user, token, permissions: userPermissions } = response.data
localStorage.setItem('token', token)
profile.value = user
permissions.value = userPermissions
await fetchPreferences()
return user
} catch (error) {
throw error
}
}
async function logout() {
try {
await api.logout()
} finally {
localStorage.removeItem('token')
$reset()
}
}
async function fetchPreferences() {
if (!profile.value) return
try {
const response = await api.getUserPreferences(profile.value.id)
preferences.value = response.data
} catch (error) {
console.error('Failed to fetch preferences:', error)
}
}
function updatePreferences(newPreferences) {
preferences.value = { ...preferences.value, ...newPreferences }
}
function $reset() {
profile.value = null
permissions.value = []
preferences.value = {}
}
return {
// State
profile: readonly(profile),
permissions: readonly(permissions),
preferences,
// Getters
isLoggedIn,
fullName,
hasPermission,
// Actions
login,
logout,
fetchPreferences,
updatePreferences,
$reset
}
})
2. Options Store模式
javascript
// stores/counter.js
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
history: []
}),
getters: {
doubleCount: (state) => state.count * 2,
countPlusOne(): number {
return this.count + 1
},
historyLength: (state) => state.history.length
},
actions: {
increment() {
this.count++
this.history.push({ action: 'increment', timestamp: Date.now() })
},
decrement() {
this.count--
this.history.push({ action: 'decrement', timestamp: Date.now() })
},
async incrementAsync() {
await new Promise(resolve => setTimeout(resolve, 1000))
this.increment()
},
reset() {
this.count = 0
this.history = []
}
}
})
3. 组合多个Store
javascript
// stores/app.js
export const useAppStore = defineStore('app', () => {
const userStore = useUserStore()
const settingsStore = useSettingsStore()
const isInitialized = ref(false)
const loading = ref(false)
async function initialize() {
if (isInitialized.value) return
loading.value = true
try {
// 并行初始化
await Promise.all([
userStore.initializeFromToken(),
settingsStore.loadSettings()
])
isInitialized.value = true
} catch (error) {
console.error('App initialization failed:', error)
} finally {
loading.value = false
}
}
function reset() {
userStore.$reset()
settingsStore.$reset()
isInitialized.value = false
}
return {
isInitialized: readonly(isInitialized),
loading: readonly(loading),
initialize,
reset
}
})
4. 插件开发
javascript
// plugins/persistence.js
function createPersistedState(options = {}) {
return ({ store }) => {
const { key = store.$id, storage = localStorage } = options
// 恢复状态
const savedState = storage.getItem(key)
if (savedState) {
store.$patch(JSON.parse(savedState))
}
// 监听变化并保存
store.$subscribe((mutation, state) => {
storage.setItem(key, JSON.stringify(state))
})
}
}
// 使用插件
const pinia = createPinia()
pinia.use(createPersistedState())
// 或者为特定store使用
const useUserStore = defineStore('user', () => {
// store定义
}, {
persist: true
})
🔧 TypeScript集成
1. 类型安全的Store
typescript
interface User {
id: number
name: string
email: string
}
interface UserState {
profile: User | null
permissions: string[]
}
export const useUserStore = defineStore('user', {
state: (): UserState => ({
profile: null,
permissions: []
}),
getters: {
isLoggedIn: (state): boolean => !!state.profile,
hasPermission: (state) => {
return (permission: string): boolean => {
return state.permissions.includes(permission)
}
}
},
actions: {
async login(credentials: LoginCredentials): Promise<User> {
const response = await api.login(credentials)
this.profile = response.data.user
this.permissions = response.data.permissions
return this.profile
}
}
})
2. 全局类型声明
typescript
// types/pinia.d.ts
import 'pinia'
declare module 'pinia' {
export interface PiniaCustomProperties {
$router: Router
$route: RouteLocationNormalizedLoaded
}
}
// 在插件中使用
pinia.use(({ store }) => {
store.$router = markRaw(router)
store.$route = markRaw(route)
})
🎯 性能优化
1. 状态订阅优化
javascript
export const useOptimizedStore = defineStore('optimized', () => {
const data = ref([])
const filters = ref({})
// 使用computed缓存昂贵计算
const filteredData = computed(() => {
return data.value.filter(item => {
return Object.entries(filters.value).every(([key, value]) => {
return !value || item[key] === value
})
})
})
// 防抖更新
const debouncedUpdateFilters = debounce((newFilters) => {
filters.value = newFilters
}, 300)
return {
data: readonly(data),
filteredData,
updateFilters: debouncedUpdateFilters
}
})
2. 懒加载Store
javascript
// 路由级别的store懒加载
const routes = [
{
path: '/admin',
component: AdminLayout,
beforeEnter: async () => {
// 动态导入store
const { useAdminStore } = await import('@/stores/admin')
const adminStore = useAdminStore()
await adminStore.initialize()
}
}
]
Pinia通过其现代化的API设计和强大的TypeScript支持,为Vue3应用提供了更优雅的状态管理解决方案。