一. 概述

1 Vuex是什么

参考官方文档

Vuex是一个集中式的状态管理插件, 实现所有组件的状态(数据)共享, 也是组件间通信的一种方式

  1. Vuex本质上是一个Vue的插件
  2. 主要实现所有组件的状态共享

2 什么时候用Vuex

image.png

一句话总结

官方推荐在中大型单页应用中使用

如何界定呢? 例如…….

  1. 需要与第三方的系统深度整合, 涉及异步获取数据, 大量使用全局变量, 业务逻辑比较复杂等情况时
  2. 如果超过3个组件需要引用/修改同一个数据结构, 比如token, 用户收藏, 购物车中的商品
  3. 涉及到深层嵌套的组件需要与其它组件进行数据通讯

二. Vuex的工作原理

1 设计思路

Vuex可以理解成一个统一管理数据的仓库, 因此通常使用store表示

理论上, 所有的组件都可以读/写store中的数据

怎样实现读数据

可以定义一个单独的全局变量, 所有的组件从全局对象中获取数据

怎样实现写数据

如果在每个组件中各自修改全局变量, 就会引起混乱, 因此设计一个工具集中的管理写操作

如何设计集中式的写操作

所有的组件不能自己修改全局变量, 需要通知Vuex, 由Vuex集中管理, 统一修改

OK, 这样Vuex里至少应该包含两个部分

  • state: 保存全局数据
  • actions: 对数据进行修改的动作

因此, 对于简单的状态管理, 我们使用类似于下面的store模式就可以

image.png

Vuex在设计时, 将动作拆分成了两个部分

  • Actions: 处理业务逻辑和异步任务
  • Mutations: 对状态的修改

image.png

2 核心概念

1) Actions

动作, 行为
对象类型, 处理逻辑和异步操作

2) Mutations

变化, 转变
对象类型, 定义方法操作数据(同步)

3) State

状态, 数据
对象类型, 保存数据

三. 起步

1 创建vue项目

使用vue脚本创建项目vuex-demo

  1. vue create vuex-demo

编写一个基本的计数器组件

  1. <template>
  2. <div>
  3. <h1>点击了{{ count }}次</h1>
  4. <button @click="increment">+1</button>
  5. <button @click="decrement">-1</button>
  6. <button @click="incrementIfOdd">是奇数就+1</button>
  7. <button @click="incrementAsync">异步+1</button>
  8. </div>
  9. </template>
  10. <script>
  11. export default {
  12. name: 'Counter',
  13. data() {
  14. return {
  15. count: 0,
  16. }
  17. },
  18. methods: {
  19. increment() {
  20. this.count++
  21. },
  22. decrement() {
  23. this.count--
  24. },
  25. incrementIfOdd() {
  26. if (this.count % 2) this.count++
  27. },
  28. incrementAsync() {
  29. setTimeout(() => {
  30. this.count++
  31. }, 500)
  32. },
  33. },
  34. }
  35. </script>

挂载到App.vue下

  1. <template>
  2. <div id="app">
  3. <counter></counter>
  4. </div>
  5. </template>
  6. <script>
  7. import Counter from '@/components/Counter.vue'
  8. export default {
  9. name: 'App',
  10. components: {
  11. Counter,
  12. },
  13. }
  14. </script>

2 安装Vuex插件

使用npm安装Vuex

  1. npm i vuex@3

:::tips 特别说明

  • 3.x的版本: 是vue2的语法
  • 4.x的版本: 是vue3的语法
  • 5.x的版本: pinia是下一代的vuex, 状态管理, 目前还在测试中 :::

    3 配置

一般在src目录下创建store/index.js

  1. // 导入Vue
  2. import Vue from 'vue'
  3. // 导入vuex
  4. import Vuex from 'vuex'
  5. // vuex本质上是vue的一个插件, 通过Vue.use()注册
  6. // 上所有的vue实例上挂载$store
  7. Vue.use(Vuex)
  8. // state: 状态
  9. const state = {}
  10. // mutations: 修改状态
  11. const mutations = {}
  12. // actions: 动作
  13. const actions = {}
  14. // 创建并导出store对象
  15. export default new Vuex.Store({
  16. state,
  17. mutations,
  18. actions,
  19. })

Store包含三个部分

  • state: 对象类型, 保存数据
  • mutations: 对象类型, 定义方法操作数据
  • actions: 对象类型, 处理逻辑和异步操作

4 导入

main.js中, 完成store对象的导入

  1. import Vue from 'vue'
  2. import App from './App.vue'
  3. import store from './store'
  4. Vue.config.productionTip = false
  5. new Vue({
  6. store, // store: store
  7. render: (h) => h(App),
  8. }).$mount('#app')

演示

image.png

四. 基本使用

为了熟悉Vuex的使用流程, 先了解单组件与Vuex如何交互

通过单组件熟悉语法和流程, 多组件使用也是类似的

1 完整流程

组件到Vuex的流程, 主要是操作, 通过向Vuex发送通知, 让Vuex修改state中的数据

1) 流程图

image.png

关键API

  • dispatch(分发, 派发): 从组件分发到Actions对象的一个方法
  • commit(提交): 提交一个Mutations对象的一个方法

2) 从组件到Actions

dispatch的两个参数

  • type: 对应action函数的名称
  • payload(可选): 携带的数据
  1. increment() {
  2. // 分发一个action, 对应调用名为increment的方法
  3. this.$store.dispatch('increment')
  4. },

发现报错

image.png

原因

actions对象中, 并没有定义对应的increment方法

解决方法

actions对象中定义increment方法

increment方法中需要调用commit, 提交一个mutation, 所以这里设计了一个context参数保留了store大部分的方法, 但是出于性能考虑, 不太需要完整的store对象, 大家可以理解成一个简化版的store

  1. increment(ctx, value) {
  2. console.log(ctx)
  3. },

通过观察, 我们发现这里也是可以拿到state

思考

能否直接在actions方法中修改state

  1. increment(ctx, value) {
  2. console.log(ctx)
  3. ctx.state.count += value
  4. },

理论上是可以的, 但是不推荐这么做, 原因后面细讲

actions里, 最核心的工作是commit到Mutations

3) 从Actions到Mutations

  1. increment(ctx, value) {
  2. ctx.commit('INCRMENT', value)
  3. },

小技巧

为了区分actionsmutations, 一般将mutations的名称大写

此时, 发现没有定义相关的mutation

image.png

解决

定义对应的mutations, 在方法中可以拿到state, 进而对state进行操作

  1. INCREMENT(state, value) {
  2. console.log(state)
  3. state.count += value
  4. },

练习

完成decrement流程

完成incrementIfOdd流程

完成incrementAsync流程

参考答案

组件的改造

在组件中通过调用dispatch分发actions

  1. methods: {
  2. increment() {
  3. // 分发一个action, 对应调用名为increment的方法
  4. this.$store.dispatch('increment', this.num)
  5. },
  6. decrement() {
  7. this.$store.dispatch('decrement', this.num)
  8. },
  9. incrementIfOdd() {
  10. this.$store.dispatch('incrementIfOdd', this.num)
  11. },
  12. incrementAsync() {
  13. this.$store.dispatch('incrementAsync', this.num)
  14. },
  15. },

Actions

  1. const actions = {
  2. increment(ctx, value) {
  3. ctx.commit('INCREMENT', value)
  4. },
  5. decrement(ctx, value) {
  6. ctx.commit('DECREMENT', value)
  7. },
  8. incrementIfOdd(ctx, value) {
  9. if (ctx.state.count % 2) ctx.commit('INCREMENTIFODD', value)
  10. },
  11. incrementAsync(ctx, value) {
  12. setTimeout(() => {
  13. ctx.commit('INCREMENTASYNC', value)
  14. }, 500)
  15. },
  16. }

Mutations

  1. const mutations = {
  2. INCREMENT(state, value) {
  3. console.log(state)
  4. state.count += value
  5. },
  6. DECREMENT(state, value) {
  7. state.count -= value
  8. },
  9. INCREMENTIFODD(state, value) {
  10. state.count += value
  11. },
  12. INCREMENTASYNC(state, value) {
  13. state.count += value
  14. },
  15. }

4) 从Vuex到组件

所有的组件对象都可以通过$store访问到Vuex, 进而可以拿到state

在组件中使用

  1. <h1>计数总和: {{ $store.state.count }}</h1>

2 简化写流程

我们发现向incrementdecrement这样的操作在actions没做什么逻辑操作, 直接转发给Mutations

所以, 我们思考是否可以跳过这个流程, 让组件直接commit到Mutations呢?

答案是肯定的, Vuex团队也注意到了这个问题

1) 流程图

image.png

2) 从组件直接到Mutations

示例

  1. methods: {
  2. increment() {
  3. // 分发一个action, 对应调用名为increment的方法
  4. // this.$store.dispatch('increment', this.num)
  5. // 直接使用commit提交给Mutations
  6. this.$store.commit('INCREMENT', this.num)
  7. },
  8. decrement() {
  9. // this.$store.dispatch('decrement', this.num)
  10. // 直接使用commit提交给Mutations
  11. this.$store.commit('DECREMENT', this.num)
  12. },
  13. },

3 深入思考

1) 为什么将Actions和Mutations分开

Vuex设计的目的就是为中大型项目服务的

  1. Actions属于分层设计, 将复杂业务解耦, 承担了类似中间件的角色
  2. Actions处理异步任务, Mutations处理同步任务
  3. Mutations方便调试与监控, 能更好地与devtools集成

案例

在组件中, 分发一个complex任务到Actions

  1. <button @click="$store.dispatch('complex')">复杂的逻辑</button>

在Actions中, 拆分成多个子任务, 完成解耦

  1. complex(ctx) {
  2. console.log('处理复杂逻辑...')
  3. ctx.dispatch('sub1')
  4. },
  5. sub1(ctx) {
  6. console.log('处理子任务1...')
  7. ctx.dispatch('sub2')
  8. },
  9. sub2(ctx) {
  10. console.log('处理子任务2...')
  11. ctx.commit('INCREMENT', 1)
  12. },

子任务可以设计成异步任务, 从而提高效率, 而真正对数据的修改统一放到Mutations中完成

2) Vuex调试工具

由于所有的数据修改统一在Mutations中完成, 只需要监控Mutations就可以完全控制state的改变, 方便大型项目中的调试, 这也是为什么不推荐在Actions中直接修改state的原因

4 Getters配置

Vuex中的Getters类似于计算属性, 用于对数据的再次加工

需求

统计已完成的todo项目

1) 定义state

  1. const state = {
  2. count: 0,
  3. todos: [
  4. { id: 1, content: '待办1', done: true },
  5. { id: 2, content: '待办2', done: false },
  6. { id: 3, content: '待办3', done: false },
  7. ],
  8. }

2) 定义getters

  1. const getters = {
  2. doneTodos: (state) => {
  3. return state.todos.filter((todo) => todo.done)
  4. },
  5. }

3) 配置getters

  1. // 创建并导出store对象
  2. export default new Vuex.Store({
  3. state,
  4. mutations,
  5. actions,
  6. getters,
  7. })

4) 在组件中调用

  1. <h3>已完成的待办</h3>
  2. <ul>
  3. <li v-for="item in $store.getters.doneTodos" :key="item.id">
  4. {{ item.content }}
  5. </li>
  6. </ul>

5) 其它用法

通过方法访问, 返回一个函数

  1. <div>id为2的todo: {{ $store.getters.getTodoById(2) }}</div>
  1. const getters = {
  2. doneTodos: (state) => {
  3. return state.todos.filter((todo) => todo.done)
  4. },
  5. getTodoById: (state) => {
  6. return (id) => {
  7. return state.todos.find((todo) => todo.id == id)
  8. }
  9. },
  10. }

五. map辅助函数

1 为什么需要辅助函数

在组件中访问state状态时, 每次都需要使用

  1. $store.state.xxx

我们希望在使用的时候直接使用xxx该如何操作呢

可以在组件中定义计算属性

  1. computed: {
  2. count() {
  3. return this.$store.state.count
  4. },
  5. name() {
  6. return this.$store.state.name
  7. },
  8. age() {
  9. return this.$store.state.age
  10. },
  11. },

我们发现computed里的函数差不多, 能否使用一个统一的写法

vuex团队也给提供了4个辅助函数

  • mapState
  • mapGetters
  • mapMutations
  • mapActions

2 mapState

为组件创建计算属性以返回 Vuex store 中的状态

先导入mapState等辅助函数

  1. import { mapState } from 'vuex'

返回一个对象, 包含了计算属性函数

  1. mounted() {
  2. // 返回一个对象
  3. console.log(mapState(['count', 'name', 'age']))
  4. },

...对象的展开语法
使用展开运算符...将对象展开完成方法的合并

  1. computed: {
  2. // 使用对象的解构赋值
  3. ...mapState(['count', 'name', 'age']),
  4. },

对象映射写法

如果希望计算属性名称和state状态名称不一致, 可以使用对象映射的写法

  1. // mapState(对象写法)
  2. ...mapState({ myCount: 'count', myName: 'name', myAge: 'age' }),

3 mapGetters

为组件创建计算属性以返回 getter 的返回值

同理, Vuex也提供了mapGetters辅助函数

  1. import { mapState, mapGetters } from 'vuex'

在computed中

  1. computed: {
  2. ...mapGetters(['doneTodos', 'getTodoById']),
  3. }

4 mapMutations

创建组件方法分发 action

在methods中

  1. ...mapMutations({ increment: 'INCREMENT', decrement: 'DECRMENT' }),

注意

这里如果要传参, 只能在调用函数时传递

  1. <button @click="increment(num)">+</button>
  2. <button @click="decrement(num)">-</button>

5 mapActions

创建组件方法提交 mutation

在methods中

  1. ...mapActions(['incrementIfOdd', 'incrementAsync', 'complex']),
  1. <button @click="incrementIfOdd(1)">是奇数就+1</button>
  2. <button @click="incrementAsync(1)">异步+1</button>
  1. <script>
  2. // 使用map辅助函数实现
  3. import { mapState, mapMutations, mapActions } from 'vuex'
  4. export default {
  5. name: 'Counter',
  6. data() {
  7. return {
  8. num: 0,
  9. }
  10. },
  11. computed: {
  12. // 对象的展开语法
  13. ...mapState(['count']),
  14. },
  15. mounted() {
  16. // mapState返回一个对象
  17. /*
  18. count: function () {
  19. return this.$store.state.count
  20. },
  21. num: function() {
  22. return this.$store.state.num
  23. },
  24. age: function() {
  25. return this.$store.state.age
  26. }
  27. */
  28. // console.log(mapState(['count', 'num', 'age']))
  29. },
  30. methods: {
  31. // increment() {
  32. // // count: 只是由本组件管理.
  33. // // 希望交给Vuex统一管理
  34. // // console.log(this.$store)
  35. // // 向Vuex分发一个action, 调用对应的actions中的方法
  36. // // this.$store.dispatch('increment')
  37. // this.$store.commit('INCREMENT')
  38. // },
  39. // decrement() {
  40. // // type: actions中的一个方法名字
  41. // // payload: 传递的数据(载荷)
  42. // // this.$store.dispatch('decrement', this.num)
  43. // this.$store.commit('DECREMENT', this.num)
  44. // },
  45. // incrementIfOdd() {
  46. // this.$store.dispatch('incrementIfOdd')
  47. // },
  48. // incrementAsync() {
  49. // this.$store.dispatch('incrementAsync')
  50. // },
  51. ...mapMutations({ increment: 'INCREMENT', decrement: 'DECREMENT' }),
  52. // ...mapActions({
  53. // incrementIfOdd: 'incrementIfOdd',
  54. // incrementAsync: 'incrementAsync',
  55. // }),
  56. // 当key和value相同时, 推荐使用数组的写法
  57. ...mapActions(['incrementIfOdd', 'incrementAsync']),
  58. },
  59. }
  60. </script>