Vue状态管理

目录


状态管理概述

为什么需要状态管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
┌─────────────────────────────────────────────────────────────┐
│ Props层层传递问题 │
│ │
│ ┌─────────┐ │
│ │ Root │ │
│ └────┬────┘ │
│ │ props │
│ ┌────▼────┐ │
│ │ Level1 │ │
│ └────┬────┘ │
│ │ props │
│ ┌────▼────┐ │
│ │ Level2 │ ──> 需要Root的数据,需要层层传递 │
│ └─────────┘ │
│ │
│ 问题: 数据流复杂,难以追踪和维护 │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ 状态管理解决方案 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Store │ │
│ │ ┌───────────────────────────────────────────────┐ │ │
│ │ │ state: { user, cart, products, ... } │ │ │
│ │ │ mutations: { updateUser, addToCart, ... } │ │ │
│ │ │ actions: { fetchUser, checkout, ... } │ │ │
│ │ └───────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ ▲ │
│ │ │
│ ┌───────────────────┼───────────────────┐ │
│ │ │ │ │
│ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ │
│ │ Root │ │ Level1 │ │ Level2 │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ 任何组件都可以直接从Store获取/修改状态 │
└─────────────────────────────────────────────────────────────┘

状态管理使用场景

场景 推荐方案
组件内部状态 data/computed/ref
父子组件通信 props/$emit
跨级组件通信 provide/inject
全局共享状态 Vuex/Pinia
服务端数据 状态管理 + 请求库

Vuex基础

Vuex核心概念

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌─────────────────────────────────────────────────────────────┐
│ Vuex 架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Actions │ ──> │Mutations│ ──> │ State │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ ▲ │ │
│ │ │ │
│ │ ┌─────────────┐ │ │
│ └──────── │ Getters │ <────────┘ │
│ └─────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Components │ │
│ │ dispatch('action') commit('mutation') │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

Vuex安装与配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Vue2 - store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
state: {
count: 0,
user: null
},
getters: {
doubleCount: state => state.count * 2
},
mutations: {
increment(state) {
state.count++
},
setUser(state, user) {
state.user = user
}
},
actions: {
async fetchUser({ commit }) {
const user = await api.getUser()
commit('setUser', user)
}
},
modules: {}
})
1
2
3
4
5
6
7
8
// Vue2 - main.js
import Vue from 'vue'
import store from './store'

new Vue({
store,
render: h => h(App)
}).$mount('#app')

State

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义State
const store = new Vuex.Store({
state: {
count: 0,
user: {
id: 1,
name: '张三',
email: 'zhangsan@example.com'
},
products: [
{ id: 1, name: '商品1', price: 100 }
]
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Vue2 - 组件中访问State
export default {
computed: {
// 方式1: 直接访问
count() {
return this.$store.state.count
},

// 方式2: mapState辅助函数
...mapState(['count', 'user', 'products'])
// 或
...mapState({
myCount: 'count',
myUser: 'user'
})
}
}

Getters

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 定义Getters
const store = new Vuex.Store({
state: {
todos: [
{ id: 1, text: '学习Vue', done: true },
{ id: 2, text: '学习Vuex', done: false },
{ id: 3, text: '学习Pinia', done: false }
]
},
getters: {
// 基本Getter
doneTodos: state => {
return state.todos.filter(todo => todo.done)
},

// Getter带参数
getTodoById: state => id => {
return state.todos.find(todo => todo.id === id)
},

// 组合Getters
doneTodosCount: (state, getters) => {
return getters.doneTodos.length
}
}
})
1
2
3
4
5
6
7
8
9
10
11
12
// 访问Getters
export default {
computed: {
// 方式1: 直接访问
doneTodos() {
return this.$store.getters.doneTodos
},

// 方式2: mapGetters辅助函数
...mapGetters(['doneTodos', 'doneTodosCount'])
}
}

Mutations

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义Mutation
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
// 参数称为payload
increment(state, payload) {
state.count += payload.amount || 1
},

decrement(state, payload) {
state.count -= payload.amount || 1
},

setUser(state, user) {
state.user = user
}
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 提交Mutation
export default {
methods: {
// 方式1: commit
increment() {
this.$store.commit('increment', { amount: 10 })
},

// 方式2: mapMutations
...mapMutations(['increment', 'setUser']),

// 组件中调用
handleClick() {
this.increment({ amount: 5 })
}
}
}

Actions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 定义Action
const store = new Vuex.Store({
state: {
count: 0,
user: null
},
mutations: {
setCount(state, count) {
state.count = count
},
setUser(state, user) {
state.user = user
}
},
actions: {
// Action接收context对象
async fetchCount({ commit }) {
try {
const count = await api.getCount()
commit('setCount', count)
} catch (error) {
console.error('获取数据失败:', error)
}
},

async fetchUser({ commit }, userId) {
const user = await api.getUser(userId)
commit('setUser', user)
return user
},

// Action返回Promise
async actionA({ commit }) {
return await api.callA()
},

async actionB({ dispatch, commit }) {
// 调用其他Action
await dispatch('actionA')
commit('setData', 'some data')
}
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
// 分发Action
export default {
methods: {
// 方式1: dispatch
async loadData() {
const result = await this.$store.dispatch('fetchCount')
console.log(result)
},

// 方式2: mapActions
...mapActions(['fetchCount', 'fetchUser'])
}
}

Mutations vs Actions

特性 Mutations Actions
同步/异步 同步 异步
修改State 直接修改 通过Mutation修改
事务调试 可以追踪 异步操作难以追踪
用途 变更状态 处理异步/业务逻辑

Pinia基础

Pinia安装

1
2
3
4
5
# Vue3 + Pinia
npm install pinia

# Vue2 + Pinia (需要piniavue2)
npm install pinia piniavue2

Pinia配置

1
2
3
4
5
6
7
8
9
10
// Vue3 - main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const pinia = createPinia()
const app = createApp(App)

app.use(pinia)
app.mount('#app')

Pinia Store创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// stores/counter.js
import { defineStore } from 'pinia'

// 方式1: Options API风格
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
user: null
}),

getters: {
doubleCount: (state) => state.count * 2
},

actions: {
increment() {
this.count++
},
async fetchUser() {
const user = await api.getUser()
this.user = user
}
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 方式2: Composition API风格
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCounterStore = defineStore('counter', () => {
// State
const count = ref(0)
const user = ref(null)

// Getters
const doubleCount = computed(() => count.value * 2)

// Actions
function increment() {
count.value++
}

async function fetchUser() {
const userData = await api.getUser()
user.value = userData
}

return { count, user, doubleCount, increment, fetchUser }
})

Pinia State使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'

const store = useCounterStore()

// 响应式访问state
console.log(store.count)
store.count++

// 使用storeToRefs保持响应式
const { count, user } = storeToRefs(store)

// 使用actions
store.increment()
store.fetchUser()
</script>

Pinia Getters

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
state: () => ({
firstName: '张',
lastName: '三',
age: 18
}),

getters: {
// 基本getter
fullName: (state) => state.firstName + state.lastName,

// 使用其他getter
displayName: (state) => {
return `${state.fullName} (${state.age}岁)`
},

// 箭头函数简写
isAdult: (state) => state.age >= 18
}
})

Pinia Actions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { defineStore } from 'pinia'
import { useCartStore } from './cart'

export const useOrderStore = defineStore('order', {
state: () => ({
orders: []
}),

actions: {
async createOrder(items) {
const cartStore = useCartStore()

const order = await api.createOrder({
items: items,
total: cartStore.totalPrice
})

this.orders.push(order)
cartStore.clearCart()

return order
}
}
})

Pinia插件

1
2
3
4
5
6
7
// plugins/piniaLogger.js
export const piniaLogger = ({ store }) => {
store.$subscribe((mutation, state) => {
console.log('State变化:', mutation.type)
console.log('State数据:', state)
})
}
1
2
3
4
5
6
// main.js
import { createPinia } from 'pinia'
import piniaLogger from './plugins/piniaLogger'

const pinia = createPinia()
pinia.use(piniaLogger)

Pinia vs Vuex

功能对比

特性 Vue3 + Vuex4 Pinia
API风格 Options/Module Options/Composition
TypeScript 需要手动类型 自动类型推断
模态化 Module Store自由组合
性能 需手动优化 自动优化
DevTools 支持 支持
体积 ~10KB ~5KB
Vue2支持 Vuex3 需piniavue2
异步追踪 mutation action

代码对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Vuex - 复杂的多模块
const store = new Vuex.Store({
modules: {
user: {
namespaced: true,
state: { name: '张三' },
getters: { fullName: state => state.name },
mutations: { setName(state, name) { state.name = name } },
actions: { fetchName({ commit }) { ... } }
}
}
})

// 访问
this.$store.state.user.name
this.$store.getters['user/fullName']
this.$store.commit('user/setName', '李四')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Pinia - 简洁的Store
export const useUserStore = defineStore('user', {
state: () => ({ name: '张三' }),
getters: {
fullName: (state) => state.name
},
actions: {
async fetchName() { ... }
}
})

// 访问
const userStore = useUserStore()
userStore.name
userStore.fullName
userStore.fetchName()

迁移建议

1
2
3
4
5
6
7
Vue2 + Vuex ──────────────────────────────────────> Vue3 + Pinia
│ │
├── Vuex modules ──────────────────────────────> Pinia stores
├── getters ────────────────────────────────────> getters
├── mutations + actions ────────────────────────> actions (统一)
├── $store ─────────────────────────────────────> useStore()
└── mapHelper ──────────────────────────────────> storeToRefs

状态管理实践

用户认证模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// stores/auth.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useAuthStore = defineStore('auth', () => {
const token = ref(localStorage.getItem('token') || '')
const userInfo = ref(null)

const isLoggedIn = computed(() => !!token.value)

async function login(credentials) {
const { token: newToken, user } = await api.login(credentials)
token.value = newToken
userInfo.value = user
localStorage.setItem('token', newToken)
}

async function logout() {
await api.logout()
token.value = ''
userInfo.value = null
localStorage.removeItem('token')
}

async function fetchUserInfo() {
if (!token.value) return
try {
userInfo.value = await api.getUserInfo()
} catch (error) {
logout()
}
}

return {
token,
userInfo,
isLoggedIn,
login,
logout,
fetchUserInfo
}
})

购物车模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// stores/cart.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCartStore = defineStore('cart', () => {
const items = ref([])

const totalPrice = computed(() => {
return items.value.reduce((sum, item) => {
return sum + item.price * item.quantity
}, 0)
})

const totalCount = computed(() => {
return items.value.reduce((sum, item) => sum + item.quantity, 0)
})

function addItem(product, quantity = 1) {
const existingItem = items.value.find(item => item.id === product.id)

if (existingItem) {
existingItem.quantity += quantity
} else {
items.value.push({
id: product.id,
name: product.name,
price: product.price,
quantity
})
}
}

function removeItem(productId) {
const index = items.value.findIndex(item => item.id === productId)
if (index > -1) {
items.value.splice(index, 1)
}
}

function updateQuantity(productId, quantity) {
const item = items.value.find(item => item.id === productId)
if (item) {
item.quantity = Math.max(0, quantity)
if (item.quantity === 0) {
removeItem(productId)
}
}
}

function clearCart() {
items.value = []
}

return {
items,
totalPrice,
totalCount,
addItem,
removeItem,
updateQuantity,
clearCart
}
})

组件中使用Store

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!-- Cart.vue -->
<template>
<div class="cart">
<h2>购物车</h2>

<div v-if="totalCount === 0">购物车是空的</div>

<div v-else>
<div v-for="item in items" :key="item.id" class="cart-item">
<span>{{ item.name }}</span>
<span>¥{{ item.price }}</span>
<input
type="number"
:value="item.quantity"
@change="updateQuantity(item.id, $event.target.value)"
>
<button @click="removeItem(item.id)">删除</button>
</div>

<div class="total">
总计: ¥{{ totalPrice }}
</div>
</div>
</div>
</template>

<script setup>
import { storeToRefs } from 'pinia'
import { useCartStore } from '@/stores/cart'

const cartStore = useCartStore()

// 使用storeToRefs保持响应式
const { items, totalPrice, totalCount } = storeToRefs(cartStore)

// 解构actions(不需要响应式)
const { addItem, removeItem, updateQuantity, clearCart } = cartStore
</script>

模块化设计

Store分离

1
2
3
4
5
6
7
stores/
├── index.js # 组合所有store
├── auth.js # 认证模块
├── user.js # 用户模块
├── cart.js # 购物车模块
├── product.js # 产品模块
└── order.js # 订单模块
1
2
3
4
5
6
7
8
9
10
11
// stores/index.js
import { createPinia } from 'pinia'
import { useAuthStore } from './auth'
import { useUserStore } from './user'
import { useCartStore } from './cart'

export {
useAuthStore,
useUserStore,
useCartStore
}

Store间相互调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// stores/order.js
import { defineStore } from 'pinia'
import { useCartStore } from './cart'

export const useOrderStore = defineStore('order', {
state: () => ({
orders: []
}),

actions: {
async checkout() {
const cartStore = useCartStore()

if (cartStore.items.length === 0) {
throw new Error('购物车为空')
}

const order = await api.createOrder({
items: cartStore.items,
total: cartStore.totalPrice
})

this.orders.push(order)
cartStore.clearCart()

return order
}
}
})

持久化存储

1
2
3
4
5
6
7
8
// stores/index.js
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

export default pinia
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// stores/auth.js
export const useAuthStore = defineStore('auth', {
state: () => ({
token: '',
userInfo: null
}),

// 持久化配置
persist: {
key: 'auth',
storage: localStorage,
paths: ['token', 'userInfo']
}
})

状态初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// stores/app.js
import { defineStore } from 'pinia'

export const useAppStore = defineStore('app', {
state: () => ({
theme: 'light',
language: 'zh-CN',
sidebarCollapsed: false,
notification: []
}),

actions: {
toggleTheme() {
this.theme = this.theme === 'light' ? 'dark' : 'light'
},

setLanguage(lang) {
this.language = lang
},

addNotification(notification) {
this.notification.push({
id: Date.now(),
...notification
})
},

removeNotification(id) {
const index = this.notification.findIndex(n => n.id === id)
if (index > -1) {
this.notification.splice(index, 1)
}
}
}
})

总结

Vuex与Pinia选择

场景 推荐
新项目Vue3 Pinia
大型企业级应用 Pinia或Vuex
Vue2项目 Vuex
需要完整调试能力 Vuex
追求轻量简洁 Pinia

状态管理最佳实践

  1. 单一数据源 - 保持Store为唯一真相源
  2. 模块化设计 - 按功能划分Store模块
  3. 同步更新 - 通过Action处理异步,Mutation修改状态
  4. 持久化处理 - 敏感数据需要持久化存储
  5. 类型支持 - 充分利用TypeScript类型推断
  6. 组件解耦 - 组件不应该直接修改全局状态

相关链接