Vuex 完全指南

Vuex介绍

什么是Vuex?

Vuex是Vue.js的官方状态管理模式和库。它作为一个集中式存储库,为应用中的所有组件提供状态管理服务,确保状态以可预测的方式进行修改。

简单来说,Vuex是一个帮助我们管理共享状态的工具,类似于React中的Redux或Angular中的NgRx。

为什么需要Vuex?

在普通的Vue应用中,组件之间共享状态的方式有:

  1. 父子组件通信:通过props向下传递,通过事件向上传递
  2. 兄弟组件通信:通过共同的父组件作为中介
  3. 跨层级组件通信:通过事件总线(EventBus)或依赖注入(provide/inject)

当应用变得复杂时,这些方式会导致以下问题:

  • 状态管理分散:状态散落在各个组件中,难以追踪和维护
  • 状态传递繁琐:组件层级深时,需要通过多层组件传递状态
  • 状态同步困难:多个组件依赖同一状态时,保持一致性变得困难
  • 状态变更不可控:任何组件都可能修改状态,导致调试困难

Vuex通过集中管理共享状态解决了这些问题:

Vuex流程图

什么时候使用Vuex?

并非所有应用都需要Vuex。如果你的应用足够简单,使用简单的store模式可能更合适。

当你的应用符合以下条件时,考虑使用Vuex:

  • 应用中有多个组件共享状态
  • 需要在组件外部改变状态
  • 需要实现复杂的状态变更逻辑
  • 应用规模中大型,需要更好的状态管理
  • 需要开发者工具支持调试状态变更

Vuex解决的核心问题

  1. 集中式状态管理:所有共享状态集中在一处管理
  2. 状态变更可追踪:通过显式的提交(commit)方式改变状态
  3. 单向数据流:状态变更遵循清晰的单向流程
  4. 模块化管理:可将大型应用的状态分割成模块
  5. 内置开发工具:时间旅行调试,状态快照导入/导出

核心概念

Vuex的核心由五个部分组成:State, Getters, Mutations, ActionsModules。它们共同构建了一个完整的状态管理系统。

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的响应式规则:

  1. 提前在store中初始化所有所需属性
  2. 当需要添加新属性时,使用以下方法之一:
// 方法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(
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

全栈凯哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值