Vue3状态管理的选型困惑:Pinia vs Composable vs Vuex
Vue3的项目架构中,状态管理方案的选择比Vue2时代更复杂。Composition API提供了reactive和ref,让组件内部状态管理变得直观;Composable函数封装可复用逻辑,天然具备状态共享能力;Pinia作为Vuex的继任者,提供DevTools集成和SSR支持。三者的职责边界如果划分不清,项目会出现状态散落、重复请求、内存泄漏等一系列问题。
核心选型原则:
– 组件私有状态 → ref/reactive(不需要外部管理)
– 跨组件共享的业务逻辑 + 服务端数据 → Pinia Store
– 跨组件共享的纯前端交互逻辑(无持久化需求) → Composable
– 遗留项目升级 → Vuex 4(不推荐新项目使用)
Pinia Store的模块化架构设计
Pinia摒弃了Vuex的mutations,简化为state/getters/actions三层结构。模块化设计的关键是按业务领域划分Store,而非按页面划分:
// stores/user.ts
import { defineStore } from 'pinia'
import type { UserProfile, LoginParams } from '@/types'
import { userApi } from '@/api/user'
export const useUserStore = defineStore('user', () => {
// State
const profile = ref<UserProfile | null>(null)
const token = ref<string>('')
const permissions = ref<string[]>([])
// Getters
const isLoggedIn = computed(() => !!token.value)
const hasPermission = (perm: string) =>
computed(() => permissions.value.includes(perm))
// Actions
async function login(params: LoginParams) {
const res = await userApi.login(params)
token.value = res.token
profile.value = res.user
permissions.value = res.permissions
}
async function fetchProfile() {
const res = await userApi.getProfile()
profile.value = res
}
function logout() {
token.value = ''
profile.value = null
permissions.value = []
}
return { profile, token, permissions, isLoggedIn, hasPermission, login, fetchProfile, logout }
})
Store间组合——一个Store可以引用另一个Store,避免状态重复:
// stores/order.ts
import { useUserStore } from './user'
export const useOrderStore = defineStore('order', () => {
const userStore = useUserStore()
const orders = ref<Order[]>([])
const userOrders = computed(() =>
orders.value.filter(o => o.userId === userStore.profile?.id)
)
return { orders, userOrders }
})
Composable函数的设计模式与边界
Composable函数适合封装可复用的交互逻辑,特点是:无全局状态、按需创建、自动清理。常见模式:
// composables/useRequest.ts — 通用请求封装
type RequestStatus = 'idle' | 'loading' | 'success' | 'error'
export function useRequest<T>(fetcher: () => Promise<T>) {
const data = ref<T | null>(null) as Ref<T | null>
const error = ref<Error | null>(null)
const status = ref<RequestStatus>('idle')
const isLoading = computed(() => status.value === 'loading')
async function execute() {
status.value = 'loading'
error.value = null
try {
data.value = await fetcher()
status.value = 'success'
} catch (e) {
error.value = e as Error
status.value = 'error'
}
}
return { data, error, status, isLoading, execute }
}
// 使用
const { data, isLoading, execute } = useRequest(() => api.getProducts())
Composable的生命周期管理——自动注册和清理副作用:
// composables/useWebSocket.ts
export function useWebSocket(url: string) {
const messages = ref<Message[]>([])
const isConnected = ref(false)
let ws: WebSocket | null = null
function connect() {
ws = new WebSocket(url)
ws.onopen = () => { isConnected.value = true }
ws.onmessage = (e) => {
messages.value.push(JSON.parse(e.data))
}
ws.onclose = () => { isConnected.value = false }
}
function disconnect() {
ws?.close()
ws = null
}
// 关键:组件卸载时自动清理
onUnmounted(() => {
disconnect()
})
return { messages, isConnected, connect, disconnect }
}
Pinia持久化与SSR场景的状态水合
Pinia状态持久化到localStorage是常见需求,但需要注意序列化边界:
// plugins/pinia-persist.ts
import type { PiniaPluginContext } from 'pinia'
export function piniaPersist({ store }: PiniaPluginContext) {
const key = `pinia-${store.$id}`
// 从localStorage恢复
const saved = localStorage.getItem(key)
if (saved) {
try {
store.$patch(JSON.parse(saved))
} catch (e) {
console.warn(`Failed to parse persisted state for ${store.$id}`)
}
}
// 监听变化并持久化(debounce避免频繁写入)
let timer: number
store.$subscribe((mutation, state) => {
clearTimeout(timer)
timer = setTimeout(() => {
localStorage.setItem(key, JSON.stringify(state))
}, 300)
})
}
// 注册
const pinia = createPinia()
pinia.use(piniaPersist)
SSR场景下的状态水合(Hydration)是Vue3 + Pinia的难点:
// entry-server.ts
export async function render(url: string) {
const { app, pinia } = createApp()
const router = app.config.globalProperties.$router
await router.push(url)
await router.isReady()
// SSR期间触发数据预取
const userStore = useUserStore(pinia)
await userStore.fetchProfile()
const html = renderToString(app)
const state = JSON.stringify(pinia.state.value)
return { html, state }
}
// entry-client.ts
const { state } = window.__PINIA_STATE__
const pinia = createPinia()
pinia.state.value = state // 水合服务端状态
app.use(pinia)
app.mount('#app')
性能优化:细粒度响应与计算属性缓存
Pinia Store中的computed属性天然具有缓存特性——依赖不变时不会重算。但React组件消费Pinia状态时,一个常见问题是不必要的重渲染:
// 问题:整个store变化都触发组件更新
const orderStore = useOrderStore()
// 模板中引用 orderStore.orders 导致任何store属性变化都重渲染
// 解决:精确订阅所需属性
const orders = computed(() => orderStore.orders)
const total = computed(() =>
orderStore.orders.reduce((s, o) => s + o.amount, 0)
)
// 或使用storeToRefs避免响应式代理的扩散
import { storeToRefs } from 'pinia'
const { orders } = storeToRefs(orderStore) // 只在orders变化时触发更新
大批量列表数据的虚拟滚动优化:
// composables/useVirtualList.ts
export function useVirtualList<T>(list: Ref<T[]>, itemHeight: number) {
const containerHeight = ref(600)
const scrollTop = ref(0)
const visibleCount = computed(() =>
Math.ceil(containerHeight.value / itemHeight)
)
const startIndex = computed(() =>
Math.floor(scrollTop.value / itemHeight)
)
const endIndex = computed(() =>
Math.min(startIndex.value + visibleCount.value, list.value.length)
)
const visibleItems = computed(() =>
list.value.slice(startIndex.value, endIndex.value)
)
const totalHeight = computed(() =>
list.value.length * itemHeight
)
const offsetY = computed(() =>
startIndex.value * itemHeight
)
return { visibleItems, totalHeight, offsetY, scrollTop }
}
状态管理不是越复杂越好。Pinia + Composable的组合已经能覆盖Vue3项目99%的状态需求。关键是严格划分职责边界——Store管业务数据和服务端状态,Composable管交互逻辑和副作用,组件ref管UI局部状态。边界清晰了,性能问题和维护性问题都会大幅减少。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-zhuang-tai-guan-li-shi-zhan-pinia-yu/