Vuex介绍
什么是Vuex?
Vuex是Vue.js的官方状态管理模式和库。它作为一个集中式存储库,为应用中的所有组件提供状态管理服务,确保状态以可预测的方式进行修改。
简单来说,Vuex是一个帮助我们管理共享状态的工具,类似于React中的Redux或Angular中的NgRx。
为什么需要Vuex?
在普通的Vue应用中,组件之间共享状态的方式有:
- 父子组件通信:通过props向下传递,通过事件向上传递
- 兄弟组件通信:通过共同的父组件作为中介
- 跨层级组件通信:通过事件总线(EventBus)或依赖注入(provide/inject)
当应用变得复杂时,这些方式会导致以下问题:
- 状态管理分散:状态散落在各个组件中,难以追踪和维护
- 状态传递繁琐:组件层级深时,需要通过多层组件传递状态
- 状态同步困难:多个组件依赖同一状态时,保持一致性变得困难
- 状态变更不可控:任何组件都可能修改状态,导致调试困难
Vuex通过集中管理共享状态解决了这些问题:
什么时候使用Vuex?
并非所有应用都需要Vuex。如果你的应用足够简单,使用简单的store模式可能更合适。
当你的应用符合以下条件时,考虑使用Vuex:
- 应用中有多个组件共享状态
- 需要在组件外部改变状态
- 需要实现复杂的状态变更逻辑
- 应用规模中大型,需要更好的状态管理
- 需要开发者工具支持调试状态变更
Vuex解决的核心问题
- 集中式状态管理:所有共享状态集中在一处管理
- 状态变更可追踪:通过显式的提交(commit)方式改变状态
- 单向数据流:状态变更遵循清晰的单向流程
- 模块化管理:可将大型应用的状态分割成模块
- 内置开发工具:时间旅行调试,状态快照导入/导出
核心概念
Vuex的核心由五个部分组成:State, Getters, Mutations, Actions和Modules。它们共同构建了一个完整的状态管理系统。
State状态
State是Vuex的核心,它是存储所有共享数据的地方。在Vuex中,我们使用单一状态树,这意味着每个应用将仅仅包含一个store实例。
在组件中访问State
基本访问方式
最简单的访问Vuex状态的方法是在计算属性中返回状态:
<template>
<div>
<p>用户名: {
{ username }}</p>
<p>年龄: {
{ age }}</p>
</div>
</template>
<script>
export default {
computed: {
username() {
return this.$store.state.user.name
},
age() {
return this.$store.state.user.age
}
}
}
</script>
使用mapState辅助函数
当需要访问多个状态时,为每个状态都写计算属性会很繁琐。我们可以使用mapState
辅助函数:
<template>
<div>
<p>用户名: {
{ username }}</p>
<p>年龄: {
{ age }}</p>
<p>是否登录: {
{ isLoggedIn }}</p>
</div>
</template>
<script>
import {
mapState } from 'vuex'
export default {
computed: {
// 映射this.username为store.state.user.name
...mapState({
username: state => state.user.name,
age: state => state.user.age,
// 使用字符串参数,等同于state => state.isLoggedIn
isLoggedIn: 'isLoggedIn'
}),
// 如果计算属性名与state子节点名相同,可以给mapState传一个字符串数组
// ...mapState(['count', 'isLoading', 'errorMessage'])
}
}
</script>
响应式规则
Vuex的state是响应式的,当我们变更状态时,监视状态的Vue组件会自动更新。这意味着我们应该遵循Vue的响应式规则:
- 提前在store中初始化所有所需属性
- 当需要添加新属性时,使用以下方法之一:
// 方法1:使用Vue.set
Vue.set(state.obj, 'newProp', 123)
// 方法2:以新对象替换老对象
state.obj = {
...state.obj, newProp: 123 }
状态变更追踪
使用Vue DevTools可以追踪状态的变化:
实际案例:用户认证状态管理
// store/index.js
import {
createStore } from 'vuex'
const store = createStore({
state: {
user: {
id: null,
name: '',
email: '',
avatar: ''
},
isAuthenticated: false,
token: localStorage.getItem('token') || '',
authError: null,
isLoading: false
},
mutations: {
setUser(state, user) {
state.user = user
state.isAuthenticated = true
},
clearUser(state) {
state.user = {
id: null,
name: '',
email: '',
avatar: ''
}
state.isAuthenticated = false
state.token = ''
},
setToken(state, token) {
state.token = token
localStorage.setItem('token', token)
},
setAuthError(state, error) {
state.authError = error
},
setLoading(state, status) {
state.isLoading = status
}
},
actions: {
async login({
commit }, credentials) {
commit('setLoading', true)
commit('setAuthError', null)
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(credentials)
})
const data = await response.json()
if (!response.ok) throw new Error(data.message || '登录失败')
commit('setToken', data.token)
commit('setUser', data.user)
} catch (error) {
commit('setAuthError', error.message)
throw error
} finally {
commit('setLoading', false)
}
},
logout({
commit }) {
commit('clearUser')
localStorage.removeItem('token')
}
}
})
export default store
在组件中使用:
<template>
<div>
<div v-if="isAuthenticated">
<img :src="user.avatar" alt="用户头像">
<h3>欢迎, {
{ user.name }}</h3>
<button @click="handleLogout">退出登录</button>
</div>
<div v-else>
<form @submit.prevent="handleLogin">
<input type="email" v-model="email" placeholder="邮箱">
<input type="password" v-model="password" placeholder="密码">
<p v-if="authError" class="error">{
{ authError }}</p>
<button type="submit" :disabled="isLoading">
{
{ isLoading ? '登录中...' : '登录' }}
</button>
</form>
</div>
</div>
</template>
<script>
import {
mapState } from 'vuex'
export default {
data() {
return {
email: '',
password: ''
}
},
computed: {
...mapState(['user', 'isAuthenticated', 'authError', 'isLoading'])
},
methods: {
async handleLogin() {
try {
await this.$store.dispatch('login', {
email: this.email,
password: this.password
})
this.$router.push('/dashboard')
} catch (error) {
// 错误已在action中处理
}
},
handleLogout() {
this.$store.dispatch('logout')
this.$router.push('/login')
}
}
}
</script>
Getters
Getters允许我们从store的state中派生出一些状态,可以认为是store的计算属性。Getters的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
基本使用
定义getter:
const store = createStore({
state: {
todos: [
{
id: 1, text: '学习Vue基础', done: true },
{
id: 2, text: '学习Vuex', done: false },
{
id: 3, text: '构建项目', done: false }
]
},
getters: {
// 获取已完成的任务
doneTodos(state) {
return state.todos.filter(todo => todo.done)
},
// 获取未完成的任务
pendingTodos(state) {
return state.todos.filter(todo => !todo.done)
},
// 获取任务总数
totalTodos(state) {
return state.todos.length
}
}
})
在组件中访问Getters
通过属性访问
Getters会暴露为store.getters
对象:
<template>
<div>
<p>已完成: {
{ doneTodosCount }} / {
{ totalTodos }}</p>
<p>完成率: {
{ completionRate }}</p>
</div>
</template>
<script>
export default {
computed: {
doneTodosCount() {
return this.$store.getters.doneTodos.length
},
totalTodos() {
return this.$store.getters.totalTodos
},
completionRate() {
const total = this.totalTodos
return total === 0
? '0%'
: `${
Math.round(this.doneTodosCount / total * 100)}%`
}
}
}
</script>
使用mapGetters辅助函数
<template>
<div>
<p>已完成: {
{ doneTodosCount }} / {
{ totalTodos }}</p>
<p>完成率: {
{ completionRate }}</p>
</div>
</template>
<script>
import {
mapGetters } from 'vuex'
export default {
computed: {
// 使用对象展开运算符将getter混入computed对象中
...mapGetters([
'doneTodos',
'pendingTodos',
'totalTodos'
]),
// 可以使用对象形式重命名getter
...mapGetters({
doneCount: 'doneTodos.length',
pendingCount: 'pendingTodos.length'
}),
// 派生计算属性
doneTodosCount() {
return this.doneTodos.length
},
completionRate() {
const total = this.totalTodos
return total === 0
? '0%'
: `${
Math.round(this.doneTodosCount / total * 100)}%`
}
}
}
</script>
Getter传参
Getter也可以返回一个函数,来实现给getter传参:
const store = createStore({
state: {
todos: [
{
id: 1, text: '学习Vue基础', done: true, category: 'study' },
{
id: 2, text: '学习Vuex', done: false, category: 'study' },
{
id: 3, text: '购物', done: false, category: 'life' }
]
},
getters: {
// 返回一个函数,可接受参数
getTodosByCategory: (state) => (category) => {
return state.todos.filter(todo => todo.category === category)
},
// 根据ID查找任务
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
}
})
在组件中使用:
// 通过方法调用并传入参数
const studyTodos = this.$store.getters.getTodosByCategory('study')
const todo = this.$store.getters.getTodoById(2)
实例:产品列表过滤和排序
// store/index.js
import {
createStore } from 'vuex'
const store = createStore({
state: {
products: [
{
id: 1, name: '笔记本电脑', price: 6000, category: '电子产品', rating: 4.5, inventory: 10 },
{
id: 2, name: '手机', price: 3000, category: '电子产品', rating: 4.2, inventory: 20 },
{
id: 3, name: '咖啡机', price: 1000, category: '家电', rating: 3.8, inventory: 5 },
{
id: 4, name: '耳机', price: 200, category: '配件', rating: 4.7, inventory: 30 },
{
id: 5, name: '键盘', price: 150, category: '配件', rating: 4.0, inventory: 15 }
],
searchQuery: '',
selectedCategory: '',
sortBy: 'name' // 'name', 'price', 'rating'
},
getters: {
// 根据搜索词和类别筛选产品
filteredProducts(state) {
return state.products.filter(product => {
// 搜索匹配
const matchesSearch = state.searchQuery === '' ||
product.name.toLowerCase().includes(state.searchQuery.toLowerCase())
// 类别匹配
const matchesCategory = state.selectedCategory === '' ||
product.category === state.selectedCategory
return matchesSearch && matchesCategory
})
},
// 排序后的产品
sortedProducts(state, getters) {
const filtered = [...getters.filteredProducts]
switch(state.sortBy) {
case 'price':
return filtered.sort((a, b) => a.price - b.price)
case 'rating':
return filtered.sort((a, b) => b.rating - a.rating)
case 'name':
default:
return filtered.sort((a, b) => a.name.localeCompare(b.name))
}
},
// 获取所有类别(不重复)
categories(state) {
return [...new Set(state.products.map(p => p.category))]
},
// 获取指定类别的产品数量
productCountByCategory: (state) => (category) => {
return state.products.filter(p => p.category === category).length
},
// 检查产品是否有库存
isInStock: (state) => (productId) => {
const product = state.products.find(p => p.id === productId)
return product && product.inventory > 0
}
},
mutations: {
setSearchQuery(state, query) {
state.searchQuery = query
},
setSelectedCategory(state, category) {
state.selectedCategory = category
},
setSortBy(state, sortBy) {
state.sortBy = sortBy
}
}
})
export default store
在组件中使用:
<template>
<div>
<div class="filters">
<input
type="text"
v-model="searchInput"
@input="updateSearch"
placeholder="搜索产品..."
/>
<select v-model="categoryFilter" @change="updateCategory">
<option value="">所有类别</option>
<option v-for="category in categories" :key="category" :value="category">
{
{ category }} ({
{ productCountByCategory(category) }})
</option>
</select>
<div class="sort-options">
<span>排序:</span>
<button
@click="updateSort('name')"
:class="{ active: sortBy === 'name' }"
>
名称
</button>
<button
@click="updateSort('price')"
:class="{ active: sortBy === 'price' }"
>
价格
</button>
<button
@click="updateSort('rating')"
:class="{ active: sortBy === 'rating' }"
>
评分
</button>
</div>
</div>
<div class="products">
<div
v-for="product in sortedProducts"
:key="product.id"
class="product-card"
>
<h3>{
{ product.name }}</h3>
<p>价格: ¥{
{ product.price }}</p>
<p>评分: {
{ product.rating }}/5</p>
<p>类别: {
{ product.category }}</p>
<p :class="{ 'out-of-stock': !isInStock(product.id) }">
{
{ isInStock(product.id) ? `库存: ${product.inventory}` : '缺货' }}
</p>
<button
:disabled="!isInStock(product.id)"
@click="addToCart(product)"
>
加入购物车
</button>
</div>
</div>
</div>
</template>
<script>
import {
mapState, mapGetters, mapMutations } from 'vuex'
export default {
data() {
return {
searchInput: '',
categoryFilter: '',
debounceTimer: null
}
},
created() {
// 初始化筛选条件
this.searchInput = this.$store.state.searchQuery
this.categoryFilter = this.$store.state.selectedCategory
},
computed: {
...mapState(['sortBy']),
...mapGetters([
'categories',
'sortedProducts',
'productCountByCategory',
'isInStock'
])
},
methods: {
...mapMutations([
'setSearchQuery',
'setSelectedCategory',
'setSortBy'
]),
updateSearch() {
// 使用防抖处理搜索
clearTimeout(this.debounceTimer)
this.debounceTimer = setTimeout(() => {
this.setSearchQuery(this.searchInput)
}, 300)
},
updateCategory() {
this.setSelectedCategory(this.categoryFilter)
},
updateSort(sortType) {
this.setSortBy(sortType)
},
addToCart(product) {
// 添加到购物车的逻辑
console.log('添加到购物车:', product.name)
}
}
}
</script>
<style>
.active {
background-color: #4CAF50;
color: white;
}
.out-of-stock {
color: red;
}
.product-card {
border: 1px solid #ccc;
padding: 15px;
margin: 10px;
border-radius: 5px;
}
</style>
Mutations
Mutations是Vuex中唯一修改状态的方式。Mutation类似于事件:每个mutation都有一个字符串的事件类型(type)和一个回调函数(handler)。回调函数是我们实际进行状态更改的地方,它接收state作为第一个参数。
基本语法
const store = createStore({
state: {
count: 0
},
mutations: {
// 不带参数的mutation
increment(state) {
state.count++
},
// 带参数的mutation
incrementBy(state, amount) {
state.count += amount
},
// 使用对象作为参数传递多个字段
updateUser(state, userData) {
state.user.name = userData.name
state.user.email = userData.email
// 更安全的写法是使用对象合并
state.user = {
...state.user, ...userData }
}
}
})
提交Mutation
方式1:直接调用commit方法
// 不带参数
store.commit('increment')
// 带参数(载荷)
store.commit('incrementBy', 10)
// 传递对象
store.commit(