十一、Vue全家桶 – Vuex状态管理

2022/8/30 Vuevuex

Vuex中文文档 (opens new window)

# 1、什么是状态管理

  • 在开发中,我们会的应用程序需要处理各种各样的数据,这些数据需要保存在我们应用程序中的某一个位置,对于这些数据的管理我们就称之为是状态管理。
  • 在前面我们是如何管理自己的状态呢?
    • 在Vue开发中,我们使用组件化的开发方式;
    • 而在组件中我们定义data或者在setup中返回使用的数据,这些数据我们称之为state;
    • 在模块template中我们可以使用这些数据,模块最终会被渲染成DOM,我们称之为View;
    • 在模块中我们会产生一些行为事件,处理这些行为事件时,有可能会修改state,这些行为事件我们称之为actions;

1382a39a24e88c5bc.png

# 2、复杂的状态管理

  • JavaScript开发的应用程序,已经变得越来越复杂了:
    • JavaScript需要管理的状态越来越多,越来越复杂;
    • 这些状态包括服务器返回的数据、缓存数据、用户操作产生的数据等等;
    • 也包括一些UI的状态,比如某些元素是否被选中,是否显示加载动效,当前分页;
  • 当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:
    • 多个视图依赖于同一状态;
    • 来自不同视图的行为需要变更同一状态;
  • 我们是否可以通过组件数据的传递来完成呢?
    • 对于一些简单的状态,确实可以通过props的传递或者Provide的方式来共享状态;
    • 但是对于复杂的状态管理来说,显然单纯通过传递和共享的方式是不足以解决问题的,比如兄弟组件如何共享数据呢?

# 3、Vuex的状态管理

  • 管理不断变化的state本身是非常困难的:

    • 状态之间相互会存在依赖,一个状态的变化会引起另一个状态的变化,View页面也有可能会引起状态的变化;
    • 当应用程序复杂时,state在什么时候,因为什么原因而发生了变化,发生了怎么样的变化,会变得非常难以控制和追踪;
  • 因此,我们是否可以考虑将组件的内部状态抽离出来,以一个全局单例的方式来管理呢?

    • 在这种模式下,我们的组件树构成了一个巨大的 “视图View”;

      不管在树的哪个位置,任何组件都能获取状态或者触发行为;

      通过定义和隔离状态管理中的各个概念,并通过强制性的规则来维护视图和状态间的独立性,我们的代码边会变得更加结构化和易于维护、跟踪(devtools);

  • 这就是Vuex背后的基本思想,它借鉴了Flux、Redux、Elm(纯函数语言,redux有借鉴它的思想);

  • 当然,目前Vue官方也在推荐使用Pinia进行状态管理,我们后续也会进行学习

2c83230e8db422a7b.png

# 4、vuex的Store对象

# 4.1.Vuex的安装

  • 依然我们要使用vuex,首先第一步需要安装vuex:
  • 我们这里使用的是vuex4.x;
## 安装状态管理
npm install vuex
1
2

# 4.2.创建Store

  • 每一个Vuex应用的核心就是store(仓库):
    • store本质上是一个容器,它包含着你的应用中大部分的状态(state);
  • Vuex和单纯的全局对象有什么区别呢?
    1. 第一:Vuex的状态存储是响应式的
      • 当Vue组件从store中读取状态的时候,若store中的状态发生变化,那么相应的组件也会被更新;
    2. 第二:你不能直接改变store中的状态
      • 改变store中的状态的唯一途径就显示提交 (commit) mutation;
      • 这样使得我们可以方便的跟踪每一个状态的变化,从而让我们能够通过一些工具帮助我们更好的管理应用的状态;
  • 使用步骤:
    • 创建Store对象;
    • 在app中通过插件安装;

3c74b8bef4df1fb8f.png

# 4.3.组件中使用store

  • 在组件中使用store,我们按照如下的方式:
    • 在模板中使用;
    • 在options api中使用,比如computed;
    • 在setup中使用;compositon API中使用
  • 以我们在store的state定义token状态为例:具体如下
<template>
  <div class="home">
    <h2>我是home组件</h2>
    <p>{{$store.state.token}}</p> <!--  1.template写法 -->
  </div>
</template>

<script>
  import { useStore } from 'vuex';
  export default {
    created() { // 2.Options API 写法
      console.log(this.$store.state.token)
    },
    setup() { // 3.Composition API 写法
      const store = useStore()
      console.log(store.state.token)
    }
  }
</script>

<style scoped>

</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 4.4.单一状态树

  • Vuex 使用单一状态树
    • 一个对象包含全部的应用层级的状态
    • 采用的是SSOT,Single Source of Truth,也可以翻译成单一数据源
  • 这也意味着,每个应用将仅仅包含一个 store 实例
    • 单状态树和模块化并不冲突,后面我们会讲到module的概念;
  • 单一状态树的优势:
    • 如果你的状态信息是保存到多个Store对象中的,那么之后的管理和维护等等都会变得特别困难;
    • 所以Vuex也使用了单一状态树来管理应用层级的全部状态;
    • 单一状态树能够让我们最直接的方式找到某个状态的片段;
    • 而且在之后的维护和调试过程中,也可以非常方便的管理和维护;
  • vuex的三大核心:state,mutations,actions

# 5、vuex中store对象的状态state

# 5.1.组件获取状态及Options API 使用mapState辅助函数

  • 在前面我们已经学习过如何在组件中获取状态了。
  • 当然,如果觉得那种方式有点繁琐(表达式过长),我们可以使用计算属性:
<script>
  export default {
    computed:{
      counter() {
        return this.$store.state.count
      }
    }
  }
</script>
1
2
3
4
5
6
7
8
9

460bd9d8b27aede92.png

# 5.2.在setup中使用mapState辅助函数

  • 在setup中如果我们单个获取装是非常简单的:
    • 通过useStore拿到store后去获取某个状态即可;
    • 但是如果我们需要使用 mapState 的功能呢?
  • 具体写法如下图(推荐这种写法):该案例是mapState的传入数组类型写法;
    • 当然也可以用传入对象类型写法具体参考上面5.1的 Options API 图片案例的 传入对象写法】

5a72109e997cbd125.png

  • 默认情况下,Vuex并没有提供非常方便的使用mapState的方式,这里我们也可以进行了一个函数的封装:

6c48a676d4000d98d.png

# 6、vuex中store对象的getters

# 6.1.getters的基本使用

  • 某些属性我们可能需要经过变化后来使用,这个时候可以使用getters:

77c9dd1cf4e9f0803.png

# 6.2.getters第二个参数

  • getters可以接收第二个参数:getters 自己本身
    • 第一个参数是:state状态
import { createStore } from 'vuex'

export default createStore({
  state(){
    return {
      //...
    }
  },
  getters:{
    totalPrice(state,getters) {
      const total_price = state.books.reduce((previousValue,currentValue) => {
        return previousValue + currentValue.count * currentValue.price
      },0)
      return total_price + "," + getter.myName
    },
    myName(state,getters) {
      return state.name
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 6.3.getters的返回函数写法

  • getters中的函数本身,也可以返回一个函数,那么在使用的地方相当于可以调用这个函数(跟计算属性类似):(调用时可以传入参数)
import { createStore } from 'vuex'

export default createStore({
  state(){
    return {
      books:[
        {name:'你不知道的Javascript',count:2,price:50},
        {name:'Javascript高级程序设计',count:6,price:100},
        {name:'追风筝的人',count:10,price:52},
      ]
      //...
    }
  },
  getters:{
    totalPrice(state,getters) {//getters中的函数本身,可以返回一个函数,那么在使用的地方相当于可以调用这个函数:
      return price => {
        const total_price = state.books.reduce((previousValue,currentValue) => {
          if( currentValue.price < price) return previousValue //书籍价格小于price的不列入求和计算
          return previousValue + currentValue.count * currentValue.price
        },0)

        return total_price
      }
    },
  }
})
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
  • 以上面store中的数据为例,我们下面进行使用
<template>
  <p>仓库中价格大于等于60的书籍一共可以卖{{$store.getters.totalPrice(60)}}元</p>
  <!-- 你会看到浏览器显示:仓库中价格大于60的书籍一共可以卖600元  -->
</template>
1
2
3
4

# 6.4.mapGetters的辅助函数

mapGetters辅助函数 (opens new window)

  • 以下代码案例 借用上面 6.2和6.3 定义的数据为基础

  • Options API 使用mapGetters的辅助函数。

<templte>
  <h2>{{ finalPrice }}</h2>
  <h2>{{ finalName }}</h2>
</templte>
<script>
  import { mapGetters } from 'vuex'
  export default {
    computed: {
      //...mapGetters['totalPrice','myName'],
      ...mapGetters[{//如果你想将一个 getter 属性另取一个名字,使用对象形式:
        finalPrice:'totalPrice',
        finalName:'myName
      }]
    }
  }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • Composition API 的setup中使用
<templte>
  <h2>{{ cTotalPrice }}</h2>
  <h2>{{ cMyName }}</h2>
</templte>
<script setup>
  import { useStore,mapGetters } from 'vuex'
  import { computed } from 'vue'
  const store = useStore
  const {totalPrice,myName} = mapGetters(['totalPrice','myName'])
  //如果你想将一个 getter 属性另取一个名字,使用对象形式:
  //const {totalPriceName} = mapGetters({
  //  totalPriceName:"totalPrice"
  //})
  const cTotalPrice = computed(totalPrice.bind({$store:store}))
  const cMyName = computed(myName.bind({$store:store}))
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

8.png

# 7、vuex中store对象的Mutations

# 7.1.Mutation基本使用

  • 更改 Vuex 的 store 中的状态(state)的唯一方法是提交 mutation:(这是vuex状态管理库约定熟成的规范)
import { createStore } from 'vuex'
export default createStore({
  state(){
    return{
      counter:0
    }
  },
  mutations:{
    increment(state){
      state.counter++
    },
    decrement(state){
      state.counter--
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 7.2.Mutation携带数据

  • 很多时候我们在提交mutation的时候,会携带一些数据,这个时候我们可以使用参数:
import { createStore } from 'vuex'
export default createStore({
  state(){
    return{
      counter:0
    }
  },
  mutations:{
    addNumber(state,payload) {//如这里payload接收到的参数为:{count:10}
      state.counter += payload.count
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 提交方式:$store.commit
    • 通过store实例对象的commit提交方法时,可以携带任何类型参数;
<template>
  <h2>counter:{{$store.state.counter}}</h2>
  <button @click='changeCounter(10)'>点击counter+10</button>
</template>
<script setup>
  import { useStore } from 'vuex'
  const store = useStore()
  function changeCounter(count) {
    store.commit('addNumber',{count})
    //store.commit({//对象风格提交方式
    //  type:'addNumber',
    //  count
    //})
  }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 7.3.Mutation常量类型

9.png

# 7.4.mapMutations辅助函数

  • 以下代码案例 借用上面7.3 定义的常量及mutations 数据为基础

  • 我们也可以借助于辅助函数,帮助我们快速映射到对应的方法中:Options API

<template>
  <!-- 在methods中定义方法,然后通过this进行使用 -->
  <button @click="changeCounter(10)">counter+10</button>

  <!-- mutations对象通过展开辅助函数,可以直接进行使用 -->
  <button @click="Add_NUMBER_YS(10)">counter+10</button>
</template>
    
<script>
  import { mapMutations } from 'vuex'
  export default {
    methods:{
      //...mapMutations(['ADD_NUMBER']),
      ...mapMutations({//将mutations里的方法映射到该组件内
        ADD_NUMBER_YS:'ADD_NUMBER'
      }),
      changeCounter(count) {
        //this.ADD_NUMBER({count})
        this.Add_NUMBER_YS({count})////由于上一步已经将mutation映射到组件内,所以组件可以直接调用Add_NUMBER_YS
      }
    }
  }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  • 在setup中使用也是一样的:Composition API
<templte>
    <h2>counter:{{$store.state.counter}}</h2>
    <button @click='changeCounter(10)'>点击counter+10</button>
</templte>
<script setup>
  import { useStore,mapMutations } from 'vuex'
  const store = useStore
  const { ADD_NUMBER } = mapMutations(['ADD_NUMBER'])
  //如果你想将一个 mutations 方法另取一个名字,使用对象形式:
  //const { ADD_NUMBER_YS } = mapMutations({
  //  ADD_NUMBER_YS:"ADD_NUMBER"
  //})
  function changeCounter(count) {
    ADD_NUMBER.bind({$store:store})({count})
    //ADD_NUMBER_YS.bind({$store:store})({count})
  }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 7.5.mutation重要原则

  • 一条重要的原则就是要记住 mutation 必须是同步函数(mutations中只能处理同步操作)
  • 这是因为devtool工具会记录mutation的日记(快照);
  • 每一条mutation被记录,devtools都需要捕捉到前一状态和后一状态的快照;
  • 但是在mutation中执行异步操作,就无法追踪到数据的变化
  • 所以Vuex的重要原则中要求 mutation必须是同步函数;
  • 但是如果我们希望在Vuex中发送网络请求的话需要如何操作呢? actions

# 8、vuex中store对象的actions

# 8.1.actions的基本使用

  • Action类似于mutation,不同在于:
    • Action提交的是mutation,而不是直接变更状态;
    • Action可以包含任意异步操作;
  • 这里有一个非常重要的参数context:
    • context是一个和store实例均有相同方法和属性的context对象;
    • 所以我们可以从其中获取到commit方法来提交一个mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters;
  • 但是为什么它不是store对象呢?这个等到我们学到Modules时再具体展开来说
import { createStore } from 'vuex'
export default createStore({
  state(){
    return{
      counter:0
    }
  },
  mutations:{
    incrementMutation(state,payload) {
      state.counter += payload
    }
  },
  actions:{
    increment(context,payload) {
      //console.log(context.state,context.getters)
      context.commit('incrementMutation',payload)
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 8.2.actions的分发操作

  • 如何使用action呢?进行action的分发:
  • 分发使用的是 store 上的dispatch函数;
  • 同样的,它也可以携带我们的参数:
  • 也可以以对象的形式进行分发:

【以下代码案例 借用上面8.1定义的数据为基础】

<template>
  <h2>counter:{{$store.state.counter}}</h2>
  <button @click="incrementCounter(1)">counter+1</button>
</template>
<script>
  export default {
    methods:{
      incrementCounter(num) {
        //this.$store.dispatch("increment",num)// 同样的,它也可以携带我们的参数(任意类型都可以):如这里的num
        
        this.$store.dispatch({//也可以以对象的形式进行分发:
          type:'increment',
          num
        })
      }
    }
  }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 8.3.actions的辅助函数

  • action也有对应的辅助函数:mapActions
    • 对象类型的写法;
    • 数组类型的写法;
  • 【以下代码案例 借用上面8.1定义的数据为基础】
  • Options API 使用mapActions辅助函数
<template>
  <div class="actions">
    <h2>我是Actions组件</h2>
    <h2>counter:{{$store.state.counter}}</h2>
    
    <!-- 在methods中定义方法,然后通过this进行使用 -->
    <button @click="increment(2)">counter+2</button>
    
    <!-- actions对象通过展开辅助函数,可以直接进行使用 -->
    <button @click="incrementThree(3)">counter+3</button>
    <button @click="incrementAction(6)">counter+6</button>
  </div>
</template>

<script>
  import { mapActions } from 'vuex';

  export default {
    methods:{
      ...mapActions(['incrementAction']),
      ...mapActions({//如果你想将一个 actions 方法另取一个名字,使用对象形式:
        incrementThree:'incrementAction'
      }),
      
      increment(num) {
        this.incrementAction(num)
      }
    }
  }
</script>
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
  • Composition API 的 setup 函数中使用mapActions辅助函数
<template>
  <div class="actions">
    <h2>我是Actions组件</h2>
    <h2>counter:{{$store.state.counter}}</h2>
    <button @click="incrementSetup(20)">counter+20</button>
    <button @click="incrementThreeSetup(30)">counter+30</button>
  </div>
</template>

<script setup>
  import { mapActions, useStore } from 'vuex';

  const store = useStore()
  //const { incrementAction } = mapActions(['incrementAction'])
  const action1 = mapActions(['incrementAction'])

  //如果你想将一个 actions 方法另取一个名字,使用对象形式:
  const { incrementThreeSetupName } = mapActions({
    incrementThreeSetupName:'incrementAction'
  })

  function incrementSetup(num) {
    // console.log(incrementAction)
    // incrementAction.bind({$store:store})(num)
    console.log(action1)
    action1.incrementAction.bind({$store:store})(num)
  }
  function incrementThreeSetup(num) {
    incrementThreeSetupName.bind({$store:store})(num)
  }
</script>
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

# 8.4.actions的异步操作

  • Action 通常是异步的,那么如何知道 action 什么时候结束呢?
    • 我们可以通过让action返回Promise,在Promise的then中来处理完成后的操作;

10.png

# 9、vuex中store对象的modules

# 9.1.module的基本使用

Module官方文档 (opens new window)

  • 什么是Module?
    • 由于使用单一状态树,应用的所有状态会集中到一个比较大的对象,当应用变得非常复杂时,store 对象就有可能变得相当臃肿;
    • 为了解决以上问题,Vuex 允许我们将 store 分割成模块(module);
    • 每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块modules

11.png

# 9.2.module的局部状态

  • 对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态state对象
import { createStore } from 'vuex'
const ADD_NUMBER = 'ADD_NUMBER'
const moduleA = {
  state(){//局部模块状态
    return{
      address:'洛杉矶',
      name:'rose'
    }
  },
  getters:{
    addressName(state,getters,rootState){//rootState是根部的状态对象
      return state.address + '-' + state.name + rootState.count //这里返回的是:洛杉矶=rose520
    }
  },
  mutations:{
    changeAddressMutation(state,payload) {
      state.address = payload
    }
  },
  actions:{
    changeAddressAction(context,payload) {
      context.commit('changeAddressMutation',payload)
    },
    //changeAddressAction({commit,rootState},payload) {
    //  commit('changeAddressMutation',payload)
    //  console.log(rootState.count)//520
    //}
  }
}

export default createStore({
  state(){//根状态
    return {
      count:520
    }
  }
  modules: {
    Submodule:moduleA
  }
})
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

# 9.3.module的命名空间

  • 默认情况下模块内部actionmutation仍然是注册在全局的命名空间中的:
    • 这样使得多个模块能够对同一个 action 或 mutation 作出响应;
    • Getter 同样也默认注册在全局命名空间
  • 如果我们希望模块具有更高的封装度和复用性,可以添加 namespaced: true 的方式使其成为带命名空间的模块
    • 当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名;

12.png

# 9.4.module修改或派发根组件

  • 如果我们希望在局部模块action中修改root中的state,那么有如下的方式:
    • 若需要在全局命名空间内分发 action 或提交 mutation,将 { root: true } 作为第三参数传给 dispatchcommit 即可。
  • Modules.vue
<template>
  <div class="modules">
    <h2>我是Modules组件</h2>
    <!-- <h2>addressName:{{$store.getters.addressName}}</h2> --><!-- 子模块未设置了:namespaced:true的情况 -->
    <h2>addressName:{{$store.getters['Submodule/addressName']}}</h2><!-- 子模块设置了:namespaced:true的情况 -->
    <h2>address:{{$store.state.Submodule.address}}</h2>
    <button @click="changeAddress('上海')">修改Submodule模块的address属性</button>
    <button @click="changeAddress1('北京')">修改Submodule模块的address属性</button>

    <!-- 根组件数据展示 -->
    <h2>rootAddress:{{$store.state.counter}}</h2>
  </div>
</template>

<script setup>
  import { useStore } from 'vuex';
  
  const store = useStore()
  //通过 store.state.Submodule 可以拿到子模块Submodule的状态
  console.log(store.state.Submodule.name)//rose
  function changeAddress(address) {  //提交子模块Submodule的mutation
    // store.commit('changeAddressMutation', address) //子模块未设置了:namespaced:true的情况
    store.commit('Submodule/changeAddressMutation', address)  //子模块设置了:namespaced:true的情况
  }
  function changeAddress1(address) {  //分发子模块Submodule的action
    // store.dispatch('changeAddressAction',address) //子模块未设置了:namespaced:true的情况
    store.dispatch('Submodule/changeAddressAction',address)  //子模块设置了:namespaced:true的情况
  }
</script>
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
  • store/index.js
import { createStore } from 'vuex'
const moduleA = {
  namespaced:true,//使该模块成为带命名空间的模块
  state(){
    return{
      address:'洛杉矶',
      name:'rose'
    }
  },
  getters:{
    addressName(state,getters,rootState,rootGetters){
      return state.address + '-' + state.name + '-' + rootState.counter
    }
  },
  mutations:{
    changeAddressMutation(state,payload) {
      state.address = payload
    }
  },
  actions:{
    changeAddressAction({rootState,commit,dispatch},payload) {
      console.log('局部模块状态2:',rootState.token)
      commit('changeAddressMutation',payload)
      
      //如果我们希望在局部模块action中修改root中的state,那么有如下的方式:
      commit('addNumber',{num:11},{root:true})//在子模块 分发 全局的mutations
      dispatch('incrementAction',52,{root:true})//在子模块 分发 全局的actions
    }
    
    //若需要在带命名空间的模块注册全局 action,你可添加 root: true,并将这个 action 的定义放在函数 handler 中。例如:
    someAction: {
     root: true,
     handler (namespacedContext, payload) { ... } // -> 'someAction'
    }
  }
}


export default createStore({
  state: {
    counter:520
  },
  mutations: {
    addNumber(state,payload) {
      state.counter += payload.num
    },
    incrementMutation(state,payload) {
      state.counter += payload
      console.log(`counter+${payload}成功`)
    }
  },
  actions: {
    incrementAction(context,payload) {
      console.log('first')
      return new Promise((resolve,reject) => {
        setTimeout(() => {
          context.commit('incrementMutation',payload)
          resolve('incrementAction执行成功')
        },1000)
      })  
    }
  },
  modules: {
    Submodule:moduleA
  }
})

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
64
65
66
67
最后更新时间: 2022/09/24, 00:24:55
彩虹
周杰伦