Vue 3 狀態管理模式與 Pinia 進階實戰 2026 | 響應式狀態完全指南

狀態管理是 Vue 應用開發的核心課題。從 Vue 2 的 Vuex 到 Vue 3 的 Pinia,再到 Composition API 的響應式 API,Vue 的狀態管理方案不斷演進。本文將深入響應式原理,系統講解 Pinia 進階用法、模塊化設計、持久化存儲及性能優化。
一、Vue 3 響應式原理
1.1 Proxy vs Object.defineProperty
// Vue 2: Object.defineProperty(只能監聽屬性訪問)
const data = {}
Object.defineProperty(data, 'count', {
get() { /* 依賴收集 */ },
set() { /* 觸發更新 */ }
})
// Vue 3: Proxy(可以監聽屬性增刪、數組變化)
const proxy = new Proxy(data, {
get(target, key) { /* 依賴收集 */ },
set(target, key, value) { /* 觸發更新 */ },
deleteProperty(target, key) { /* 觸發更新 */ }
})1.2 ref 和 reactive 的區別
import { ref, reactive, watch, watchEffect } from 'vue'
// ref:用於基本類型和對象
const count = ref(0)
count.value++ // 需要 .value
const user = ref({ name: 'Alice' })
user.value.name = 'Bob' // 對象內部響應式
// reactive:僅用於對象
const state = reactive({ count: 0 })
state.count++ // 不需要 .value
// watch:監聽特定數據源
watch(count, (newVal, oldVal) => {
console.log(`count 從 ${oldVal} 變為 ${newVal}`)
})
// watchEffect:自動追蹤依賴
watchEffect(() => {
console.log(`當前 count: ${count.value}`)
})1.3 shallowRef 和 shallowReactive
import { shallowRef, shallowReactive } from 'vue'
// shallowRef:只監聽 .value 本身的變化
const shallowUser = shallowRef({ name: 'Alice' })
shallowUser.value.name = 'Bob' // 不會觸發更新!
shallowUser.value = { name: 'Bob' } // 會觸發更新
// shallowReactive:只監聽第一層屬性變化
const shallowState = shallowReactive({
nested: { count: 0 }
})
shallowState.nested.count++ // 不會觸發更新!
shallowState.nested = { count: 1 } // 會觸發更新1.4 computed 和 watch 的選擇
import { ref, computed, watch } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
// computed:用於派生值,有緩存
const fullName = computed(() => {
console.log('計算 fullName')
return `${firstName.value} ${lastName.value}`
})
// watch:用於副作用,無緩存
watch([firstName, lastName], ([newFirst, newLast]) => {
console.log(`名字變為: ${newFirst} ${newLast}`)
// 發送到服務器、更新 localStorage 等
})二、Pinia 基礎
2.1 什麼是 Pinia
Pinia 是 Vue 官方推薦的狀態管理庫,具有以下特點:
- 直觀的 API(類似組件)
- 完整的 TypeScript 支持
- 模塊化設計(每個 store 獨立)
- 無 mutations(直接修改狀態)
- 支持插件系統
2.2 安裝與配置
npm install pinia// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.mount('#app')2.3 創建 Store
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
// 狀態(state)
const name = ref('')
const email = ref('')
const isLoggedIn = ref(false)
// 計算屬性(getters)
const displayName = computed(() => {
return name.value || 'Guest'
})
// 方法(actions)
async function login(username: string, password: string) {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ username, password })
})
const data = await response.json()
name.value = data.name
email.value = data.email
isLoggedIn.value = true
}
function logout() {
name.value = ''
email.value = ''
isLoggedIn.value = false
}
return { name, email, isLoggedIn, displayName, login, logout }
})2.4 在組件中使用
<script setup lang="ts">
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// 直接訪問狀態
console.log(userStore.name)
// 調用方法
await userStore.login('admin', 'password')
// 解構狀態(保持響應式)
import { storeToRefs } from 'pinia'
const { name, isLoggedIn } = storeToRefs(userStore)
</script>三、Pinia 進階技巧
3.1 模塊化 Store 設計
// stores/index.ts - 導出所有 store
export { useUserStore } from './user'
export { useCartStore } from './cart'
export { useProductStore } from './product'
// stores/user.ts - 用戶模塊
export const useUserStore = defineStore('user', () => { /* ... */ })
// stores/cart.ts - 購物車模塊
export const useCartStore = defineStore('cart', () => {
const items = ref<CartItem[]>([])
const totalPrice = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
const totalCount = computed(() =>
items.value.reduce((sum, item) => sum + item.quantity, 0)
)
function addItem(product: Product) {
const existing = items.value.find(item => item.id === product.id)
if (existing) {
existing.quantity++
} else {
items.value.push({ ...product, quantity: 1 })
}
}
function removeItem(productId: string) {
const index = items.value.findIndex(item => item.id === productId)
if (index !== -1) {
items.value.splice(index, 1)
}
}
function clearCart() {
items.value = []
}
return { items, totalPrice, totalCount, addItem, removeItem, clearCart }
})3.2 Store 間通信
// stores/order.ts - 訂單模塊依賴購物車模塊
import { defineStore } from 'pinia'
import { useCartStore } from './cart'
export const useOrderStore = defineStore('order', () => {
const orders = ref<Order[]>([])
async function createOrder() {
const cartStore = useCartStore()
if (cartStore.totalCount === 0) {
throw new Error('購物車為空')
}
const order: Order = {
id: Date.now().toString(),
items: [...cartStore.items],
total: cartStore.totalPrice,
createdAt: new Date()
}
await fetch('/api/orders', {
method: 'POST',
body: JSON.stringify(order)
})
orders.value.push(order)
cartStore.clearCart() // 清空購物車
}
return { orders, createOrder }
})3.3 狀態持久化
// 使用 pinia-plugin-persistedstate
npm install pinia-plugin-persistedstate// main.ts
import { createPinia } from 'pinia'
import persistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(persistedstate)// stores/user.ts - 配置持久化
export const useUserStore = defineStore('user', () => {
// ...
}, {
persist: {
key: 'user-store',
storage: localStorage, // 默認 localStorage
paths: ['name', 'email', 'isLoggedIn'] // 只持久化指定屬性
}
})自定義持久化邏輯:
export const useUserStore = defineStore('user', () => {
const name = ref('')
// 從 localStorage 初始化
if (localStorage.getItem('user-name')) {
name.value = localStorage.getItem('user-name')!
}
// watch 監聽變化自動保存
watch(name, (newVal) => {
localStorage.setItem('user-name', newVal)
})
return { name }
})3.4 全局插件
// plugins/logger.ts
export function loggerPlugin() {
return (context: PiniaPluginContext) => {
const { store } = context
// 記錄所有 action 調用
store.$onAction(({ name, args, after, onError }) => {
console.log(`[Pinia] ${store.$id}.${name} 被調用,參數:`, args)
after((result) => {
console.log(`[Pinia] ${store.$id}.${name} 成功,返回:`, result)
})
onError((error) => {
console.error(`[Pinia] ${store.$id}.${name} 失敗:`, error)
})
})
}
}
// main.ts
pinia.use(loggerPlugin())3.5 狀態重置
// stores/user.ts
export const useUserStore = defineStore('user', () => {
const name = ref('')
const email = ref('')
const isLoggedIn = ref(false)
// 定義初始狀態
const resetState = () => {
name.value = ''
email.value = ''
isLoggedIn.value = false
}
return { name, email, isLoggedIn, resetState }
})
// 使用
const userStore = useUserStore()
userStore.resetState()
// 或使用內置方法(需要定義 $reset)
userStore.$reset()四、組合式狀態管理模式
4.1 使用 Composition API 管理局部狀態
// composables/useCounter.ts
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const doubled = computed(() => count.value * 2)
function increment() {
count.value++
}
function decrement() {
count.value--
}
function reset() {
count.value = initialValue
}
return { count, doubled, increment, decrement, reset }
}
// 在組件中使用
<script setup>
const { count, doubled, increment } = useCounter(10)
</script>4.2 組合式 Store(無 Pinia)
// stores/todo.ts - 使用 reactive 實現輕量級狀態管理
import { reactive, computed } from 'vue'
const state = reactive({
todos: [] as Todo[],
filter: 'all' as 'all' | 'active' | 'completed'
})
export function useTodoStore() {
const filteredTodos = computed(() => {
switch (state.filter) {
case 'active':
return state.todos.filter(todo => !todo.completed)
case 'completed':
return state.todos.filter(todo => todo.completed)
default:
return state.todos
}
})
const activeCount = computed(() =>
state.todos.filter(todo => !todo.completed).length
)
function addTodo(text: string) {
state.todos.push({
id: Date.now(),
text,
completed: false
})
}
function toggleTodo(id: number) {
const todo = state.todos.find(t => t.id === id)
if (todo) {
todo.completed = !todo.completed
}
}
function setFilter(filter: 'all' | 'active' | 'completed') {
state.filter = filter
}
return {
todos: () => state.todos,
filter: () => state.filter,
filteredTodos,
activeCount,
addTodo,
toggleTodo,
setFilter
}
}五、性能優化
5.1 避免不必要的計算
// ❌ 每次渲染都會重新計算
const expensiveResult = computed(() => {
// 耗時操作
return heavyComputation(state.data)
})
// ✅ 使用緩存
import { ref, computed } from 'vue'
const cache = ref<Map<string, Result>>(new Map())
const expensiveResult = computed(() => {
const key = JSON.stringify(state.data)
if (cache.value.has(key)) {
return cache.value.get(key)!
}
const result = heavyComputation(state.data)
cache.value.set(key, result)
return result
})5.2 使用 watch 替代 computed
// ❌ computed 用於副作用
const saveToServer = computed(() => {
fetch('/api/save', {
method: 'POST',
body: JSON.stringify(state.data)
})
})
// ✅ 使用 watch
watch(() => state.data, async (newData) => {
await fetch('/api/save', {
method: 'POST',
body: JSON.stringify(newData)
})
}, { deep: true })5.3 優化列表渲染
<script setup>
import { computed } from 'vue'
import { useProductStore } from '@/stores/product'
const productStore = useProductStore()
// 只計算一次
const products = computed(() => [...productStore.products])
</script>
<template>
<!-- 使用 v-for 時提供唯一 key -->
<div v-for="product in products" :key="product.id">
{{ product.name }}
</div>
</template>5.4 緩存 Store 實例
// stores/user.ts
let userStoreInstance: ReturnType<typeof useUserStore> | null = null
export function useUserStore() {
if (!userStoreInstance) {
userStoreInstance = defineStore('user', () => { /* ... */ })()
}
return userStoreInstance
}六、Pinia vs Vuex
6.1 核心差異
| 特性 | Vuex | Pinia |
|---|---|---|
| 狀態修改 | 必須通過 mutations | 直接修改 |
| 模塊化 | modules + namespaced | 天然模塊化 |
| TypeScript | 需要手動聲明類型 | 完整推斷 |
| API | 複雜(state/mutations/actions/getters) | 簡潔(state/computed/actions) |
| 代碼組織 | 單一 store 大文件 | 每個 store 獨立文件 |
| 插件系統 | 有限 | 強大 |
6.2 遷移指南
Vuex 代碼:
// store/modules/user.ts
const state = {
name: '',
isLoggedIn: false
}
const mutations = {
SET_NAME(state, name) {
state.name = name
},
LOGIN(state) {
state.isLoggedIn = true
},
LOGOUT(state) {
state.name = ''
state.isLoggedIn = false
}
}
const actions = {
async login({ commit }, { username, password }) {
const data = await api.login(username, password)
commit('SET_NAME', data.name)
commit('LOGIN')
}
}
const getters = {
displayName: state => state.name || 'Guest'
}
export default {
namespaced: true,
state,
mutations,
actions,
getters
}Pinia 等價代碼:
// stores/user.ts
export const useUserStore = defineStore('user', () => {
const name = ref('')
const isLoggedIn = ref(false)
const displayName = computed(() => name.value || 'Guest')
async function login(username: string, password: string) {
const data = await api.login(username, password)
name.value = data.name
isLoggedIn.value = true
}
function logout() {
name.value = ''
isLoggedIn.value = false
}
return { name, isLoggedIn, displayName, login, logout }
})6.3 遷移步驟
- 安裝 Pinia:
npm install pinia - 創建 Pinia 實例:在
main.ts中配置 - 逐個遷移模塊:將 Vuex modules 轉換為 Pinia stores
- 更新組件引用:
this.$store.dispatch('user/login')→useUserStore().login() - 移除 Vuex:刪除 Vuex 相關依賴和配置
- 測試驗證:確保所有功能正常
七、最佳實踐
7.1 Store 組織原則
- 單一職責:每個 store 只管理一個業務領域
- 扁平結構:避免嵌套 store
- 可複用性:將通用邏輯提取為 composables
- 測試友好:確保 store 可以獨立測試
7.2 狀態管理選擇指南
| 場景 | 推薦方案 |
|---|---|
| 全局共享狀態 | Pinia |
| 組件間通信 | props + emit 或 Pinia |
| 頁面級狀態 | Composition API(composables) |
| 表單狀態 | 組件局部狀態 |
| 路由狀態 | Vue Router query/params |
7.3 常見錯誤
// ❌ 錯誤:直接解構會丟失響應式
const { name } = useUserStore() // name 不是響應式的
// ✅ 正確:使用 storeToRefs
const { name } = storeToRefs(useUserStore())
// ❌ 錯誤:在 action 中使用 await 但沒有 async
function fetchData() {
await api.getData() // 報錯!
}
// ✅ 正確:action 必須是 async
async function fetchData() {
await api.getData()
}
// ❌ 錯誤:在組件外調用 store 方法時沒有初始化
// 必須在組件或 setup 函數中調用 useStore()八、總結
- ✅ 深入理解 Vue 3 響應式原理(Proxy、ref、reactive)
- ✅ 掌握 Pinia 基礎用法(state、computed、actions)
- ✅ 實戰模塊化 Store 設計與 Store 間通信
- ✅ 實現狀態持久化與全局插件
- ✅ 使用 Composition API 管理局部狀態
- ✅ 性能優化技巧(緩存、watch、列表渲染)
- ✅ Pinia 與 Vuex 對比及遷移指南
狀態管理的核心原則是:保持簡單,按需選擇。對於大多數應用,Pinia + Composition API 的組合已經足夠強大。
相關閱讀: