Skip to content

Vuex 详解

1. 什么是 Vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

Vuex 的核心概念包括:

  • State:存储应用状态
  • Getters:从 State 中派生出的计算属性
  • Mutations:修改 State 的唯一方式
  • Actions:处理异步操作
  • Modules:将状态管理分割成模块

2. 安装与基本使用

安装

bash
# 使用 npm
npm install vuex@3

# 使用 yarn
yarn add vuex@3

基本使用

javascript
// 1. 导入 Vue 和 Vuex
import Vue from 'vue'
import Vuex from 'vuex'

// 2. 注册 Vuex 插件
Vue.use(Vuex)

// 3. 创建 store
const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++
    },
    decrement(state) {
      state.count--
    }
  },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => {
        commit('increment')
      }, 1000)
    }
  },
  getters: {
    doubleCount: state => state.count * 2
  }
})

// 4. 在 Vue 实例中使用
new Vue({
  store
}).$mount('#app')

在组件中使用:

javascript
// 方式 1:直接访问
this.$store.state.count
this.$store.getters.doubleCount

// 方式 2:使用辅助函数
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'

export default {
  computed: {
    ...mapState(['count']),
    ...mapGetters(['doubleCount'])
  },
  methods: {
    ...mapMutations(['increment', 'decrement']),
    ...mapActions(['incrementAsync'])
  }
}

3. 核心 API

State

  • 作用:存储应用的状态。
  • 使用场景:用于存储需要在多个组件间共享的数据。
javascript
const store = new Vuex.Store({
  state: {
    count: 0,
    user: {
      name: 'John',
      age: 30
    }
  }
})

Getters

  • 作用:从 State 中派生出的计算属性。
  • 使用场景:用于处理需要基于 State 进行计算的数据。
javascript
const store = new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: '学习 Vue', done: true },
      { id: 2, text: '学习 Vuex', done: false }
    ]
  },
  getters: {
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    },
    doneTodosCount: (state, getters) => {
      return getters.doneTodos.length
    },
    getTodoById: state => id => {
      return state.todos.find(todo => todo.id === id)
    }
  }
})

Mutations

  • 作用:修改 State 的唯一方式。
  • 特点:同步操作。
  • 使用场景:用于同步修改状态。
javascript
const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment(state, payload) {
      state.count += payload || 1
    }
  }
})

// 调用 mutation
store.commit('increment')
store.commit('increment', 10) // 带 payload
// 或使用对象风格的提交方式
store.commit({ type: 'increment', amount: 10 })

Actions

  • 作用:处理异步操作。
  • 特点:可以包含异步操作,通过 commit mutations 来修改状态。
  • 使用场景:用于处理需要异步操作的逻辑,如 API 请求。
javascript
const store = new Vuex.Store({
  state: {
    posts: []
  },
  mutations: {
    setPosts(state, posts) {
      state.posts = posts
    }
  },
  actions: {
    async fetchPosts({ commit }) {
      const response = await axios.get('/api/posts')
      commit('setPosts', response.data)
    }
  }
})

// 调用 action
store.dispatch('fetchPosts')
// 带参数
store.dispatch('fetchPosts', { userId: 1 })
// 或使用对象风格的分发方式
store.dispatch({ type: 'fetchPosts', userId: 1 })

Modules

  • 作用:将状态管理分割成模块。
  • 使用场景:用于大型应用,将状态管理按功能或业务领域分割。
javascript
const userModule = {
  namespaced: true,
  state: {
    user: null
  },
  mutations: {
    setUser(state, user) {
      state.user = user
    }
  },
  actions: {
    async login({ commit }, credentials) {
      const response = await axios.post('/api/login', credentials)
      commit('setUser', response.data)
    }
  },
  getters: {
    isLoggedIn: state => !!state.user
  }
}

const store = new Vuex.Store({
  modules: {
    user: userModule
  }
})

// 访问模块状态
store.state.user.user

// 调用模块的 mutation
store.commit('user/setUser', { name: 'John' })

// 调用模块的 action
store.dispatch('user/login', { username: 'john', password: '123' })

// 访问模块的 getter
store.getters['user/isLoggedIn']

4. 实现原理

核心原理

Vuex 的核心原理基于以下几点:

  1. 集中式存储:所有状态都存储在一个集中的 store 中。
  2. 响应式系统:利用 Vue 的响应式系统,当状态变化时,依赖于这些状态的组件会自动更新。
  3. 严格的状态变更规则:通过 mutations 来修改状态,确保状态变更可追踪。
  4. 异步处理:通过 actions 来处理异步操作,保持 mutations 的同步性。

源码解析

Store 构造函数

javascript
class Store {
  constructor(options = {}) {
    // 初始化状态
    this._state = new Vue({
      data: {
        $$state: options.state || {}
      }
    })

    // 初始化 mutations
    this._mutations = Object.create(null)
    // 初始化 actions
    this._actions = Object.create(null)
    // 初始化 getters
    this._wrappedGetters = Object.create(null)

    // 处理 modules
    this._modules = new ModuleCollection(options)

    // 安装模块
    installModule(this, state, [], this._modules.root)

    // 启用严格模式
    if (options.strict) {
      enableStrictMode(this)
    }
  }

  // 获取状态
  get state() {
    return this._state._data.$$state
  }

  // 提交 mutation
  commit(type, payload) {
    const entry = this._mutations[type]
    if (!entry) {
      console.error(`[vuex] unknown mutation type: ${type}`)
      return
    }
    entry.forEach(function commitIterator(handler) {
      handler(payload)
    })
  }

  // 分发 action
  dispatch(type, payload) {
    const entry = this._actions[type]
    if (!entry) {
      console.error(`[vuex] unknown action type: ${type}`)
      return
    }
    return entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)
  }
}

响应式状态

Vuex 利用 Vue 的响应式系统来实现状态的响应式:

javascript
// 创建响应式状态
this._state = new Vue({
  data: {
    $$state: options.state || {}
  }
})

当状态发生变化时,Vue 的响应式系统会自动通知依赖于这些状态的组件。

Mutation 提交

Mutation 是修改状态的唯一方式,且必须是同步操作:

javascript
commit(type, payload) {
  const entry = this._mutations[type]
  if (!entry) {
    console.error(`[vuex] unknown mutation type: ${type}`)
    return
  }
  entry.forEach(function commitIterator(handler) {
    handler(payload)
  })
}

Action 分发

Action 用于处理异步操作,最终通过 commit mutations 来修改状态:

javascript
dispatch(type, payload) {
  const entry = this._actions[type]
  if (!entry) {
    console.error(`[vuex] unknown action type: ${type}`)
    return
  }
  return entry.length > 1
    ? Promise.all(entry.map(handler => handler(payload)))
    : entry[0](payload)
}

模块化实现

Vuex 通过 ModuleCollection 来处理模块化:

javascript
class ModuleCollection {
  constructor(rawRootModule) {
    // 注册根模块
    this.register([], rawRootModule, false)
  }

  register(path, rawModule, runtime) {
    // 创建模块
    const newModule = new Module(rawModule, runtime)

    if (path.length === 0) {
      this.root = newModule
    } else {
      const parent = this.get(path.slice(0, -1))
      parent.addChild(path[path.length - 1], newModule)
    }

    // 注册子模块
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }
}

5. 常见问题及解决方案

1. 状态更新但组件未更新

问题:修改状态后,组件没有更新。

原因

  • 直接修改了 state 对象,而不是通过 mutations。
  • 在 state 中添加了新的属性,但没有使用 Vue.set。

解决方案

  • 始终通过 mutations 修改状态。
  • 使用 Vue.set 或展开运算符添加新属性。
javascript
// 错误的做法
store.state.count = 1

// 正确的做法
store.commit('increment')

// 添加新属性的正确做法
// 在 mutation 中
Vue.set(state.user, 'age', 30)
// 或
state.user = { ...state.user, age: 30 }

2. 异步操作导致的问题

问题:在 mutations 中执行异步操作,导致状态更新不可预测。

原因:mutations 应该是同步的,异步操作会导致调试困难。

解决方案:将异步操作移到 actions 中,通过 commit mutations 来修改状态。

javascript
// 错误的做法
mutations: {
  async fetchData(state) {
    const data = await axios.get('/api/data')
    state.data = data
  }
}

// 正确的做法
actions: {
  async fetchData({ commit }) {
    const data = await axios.get('/api/data')
    commit('setData', data)
  }
},
mutations: {
  setData(state, data) {
    state.data = data
  }
}

3. 模块化命名空间问题

问题:在模块化中,访问模块的状态、getters、mutations 或 actions 时出现命名冲突。

原因:模块没有启用命名空间。

解决方案:在模块中添加 namespaced: true

javascript
const userModule = {
  namespaced: true // 启用命名空间
  // ...
}

// 访问时需要使用命名空间
store.commit('user/setUser', user)
store.dispatch('user/login', credentials)
store.getters['user/isLoggedIn']

4. 性能问题

问题:当状态较大时,每次状态变更都会导致所有组件重新渲染。

原因:组件直接访问 $store.state,没有使用计算属性或辅助函数。

解决方案

  • 使用计算属性或 mapState 辅助函数。
  • 对于大型状态,考虑使用模块化。
  • 使用 Object.freeze 冻结不需要修改的状态。
javascript
// 推荐的做法
export default {
  computed: {
    ...mapState({
      count: state => state.count
    }),
    ...mapGetters(['doubleCount'])
  }
}

5. 调试困难

问题:状态变更难以追踪,调试困难。

原因:没有使用 Vuex 的调试工具。

解决方案

  • 使用 Vue DevTools 进行调试。
  • 启用严格模式,在开发环境中捕获直接修改状态的错误。
javascript
const store = new Vuex.Store({
  // ...
  strict: process.env.NODE_ENV !== 'production'
})

6. 最佳实践

1. 状态设计

  • 单一数据源:所有状态都应该集中在 store 中。
  • 状态扁平化:避免深层嵌套的状态结构,使用扁平化的结构。
  • 使用常量:使用常量定义 mutation 和 action 类型,避免拼写错误。
javascript
// mutation-types.js
export const INCREMENT = 'INCREMENT'
export const DECREMENT = 'DECREMENT'

// store.js
import { INCREMENT, DECREMENT } from './mutation-types'

const store = new Vuex.Store({
  mutations: {
    [INCREMENT](state) {
      state.count++
    },
    [DECREMENT](state) {
      state.count--
    }
  }
})

2. 模块化

  • 按功能分割:将状态管理按功能或业务领域分割成模块。
  • 启用命名空间:为每个模块启用命名空间,避免命名冲突。
  • 模块嵌套:对于复杂的应用,可以嵌套模块。

3. 异步操作

  • 在 actions 中处理异步:所有异步操作都应该放在 actions 中。
  • 使用 async/await:使用 async/await 来处理异步操作,使代码更清晰。
  • 错误处理:在 actions 中处理错误,并通过返回 Promise 传递错误。
javascript
actions: {
  async fetchData({ commit }) {
    try {
      const response = await axios.get('/api/data')
      commit('setData', response.data)
      return response.data
    } catch (error) {
      console.error('Error fetching data:', error)
      throw error
    }
  }
}

4. 性能优化

  • 使用计算属性:使用 getters 或计算属性来缓存计算结果。
  • 避免直接修改状态:始终通过 mutations 修改状态。
  • 使用 Vue DevTools:使用 Vue DevTools 进行性能分析。

5. 测试

  • 测试 mutations:测试 mutations 是否正确修改状态。
  • 测试 actions:测试 actions 是否正确处理异步操作和 commit mutations。
  • 测试 getters:测试 getters 是否正确计算派生状态。

7. 完整示例

基本结构

javascript
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user'
import posts from './modules/posts'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    loading: false
  },
  mutations: {
    setLoading(state, status) {
      state.loading = status
    }
  },
  actions: {
    setLoading({ commit }, status) {
      commit('setLoading', status)
    }
  },
  getters: {
    isLoading: state => state.loading
  },
  modules: {
    user,
    posts
  }
})

// store/modules/user.js
export default {
  namespaced: true,
  state: {
    user: null,
    token: null
  },
  mutations: {
    setUser(state, user) {
      state.user = user
    },
    setToken(state, token) {
      state.token = token
    },
    logout(state) {
      state.user = null
      state.token = null
    }
  },
  actions: {
    async login({ commit }, credentials) {
      try {
        const response = await axios.post('/api/login', credentials)
        commit('setUser', response.data.user)
        commit('setToken', response.data.token)
        localStorage.setItem('token', response.data.token)
        return response.data
      } catch (error) {
        throw error
      }
    },
    logout({ commit }) {
      commit('logout')
      localStorage.removeItem('token')
    }
  },
  getters: {
    isLoggedIn: state => !!state.token,
    currentUser: state => state.user
  }
}

// store/modules/posts.js
export default {
  namespaced: true,
  state: {
    posts: [],
    currentPost: null
  },
  mutations: {
    setPosts(state, posts) {
      state.posts = posts
    },
    setCurrentPost(state, post) {
      state.currentPost = post
    }
  },
  actions: {
    async fetchPosts({ commit }) {
      try {
        const response = await axios.get('/api/posts')
        commit('setPosts', response.data)
        return response.data
      } catch (error) {
        throw error
      }
    },
    async fetchPost({ commit }, id) {
      try {
        const response = await axios.get(`/api/posts/${id}`)
        commit('setCurrentPost', response.data)
        return response.data
      } catch (error) {
        throw error
      }
    }
  },
  getters: {
    allPosts: state => state.posts,
    currentPost: state => state.currentPost
  }
}

在组件中使用

javascript
<template>
  <div>
    <div v-if="isLoading">加载中...</div>
    <div v-else>
      <h1>Posts</h1>
      <ul>
        <li v-for="post in allPosts" :key="post.id">
          {{ post.title }}
          <button @click="fetchPost(post.id)">查看详情</button>
        </li>
      </ul>
      <div v-if="currentPost">
        <h2>{{ currentPost.title }}</h2>
        <p>{{ currentPost.content }}</p>
      </div>
      <div v-if="!isLoggedIn">
        <button @click="login">登录</button>
      </div>
      <div v-else>
        <p>欢迎, {{ currentUser.name }}</p>
        <button @click="logout">登出</button>
      </div>
    </div>
  </div>
</template>

<script>
import { mapState, mapGetters, mapActions } from 'vuex'

export default {
  computed: {
    ...mapState(['loading']),
    ...mapGetters('user', ['isLoggedIn', 'currentUser']),
    ...mapGetters('posts', ['allPosts', 'currentPost'])
  },
  methods: {
    ...mapActions(['setLoading']),
    ...mapActions('user', ['login', 'logout']),
    ...mapActions('posts', ['fetchPosts', 'fetchPost']),
    async mounted() {
      this.setLoading(true)
      try {
        await this.fetchPosts()
      } catch (error) {
        console.error('Error fetching posts:', error)
      } finally {
        this.setLoading(false)
      }
    }
  }
}
</script>

8. 总结

Vuex 是一个强大的状态管理库,通过集中式存储和严格的状态变更规则,使应用的状态管理更加可预测和可维护。

核心概念包括:

  • State:存储应用状态
  • Getters:派生状态的计算属性
  • Mutations:同步修改状态
  • Actions:处理异步操作
  • Modules:模块化状态管理

实现原理基于 Vue 的响应式系统,通过集中式存储和严格的状态变更规则,确保状态的可预测性。

在使用 Vuex 时,应遵循最佳实践:

  • 状态设计要合理,避免深层嵌套
  • 使用模块化和命名空间
  • 在 actions 中处理异步操作
  • 始终通过 mutations 修改状态
  • 使用计算属性和辅助函数
  • 启用严格模式进行调试

通过合理使用 Vuex,可以构建出更加可维护、可扩展的 Vue 应用。

基于 VitePress 的本地知识库