[TOC]

什么是状态管理

在开发中,我们的应用程序需要处理各种各样的数据,这些数据需要保存在我们应用程序中的某一个位置,对于这些数据的管理我们就称之为是状态管理。

在之前我们是如何管理自己的状态的?

  • 在Vue开发中,我们使用组件化的开发方式
  • 而在组件中我们定义data或者在setup中返回使用的数据,这些数据我们称之为state
  • 在模块template中我们可以使用这些数据,模块最终会被渲染成DOM,我们称之为View
  • 在模块中我们会产生一些行为事件,处理这些行为事件时,有可能会修改state,这些行为事件我们称之为actions

Vuex - 图1

复杂的状态管理

JS开发的应用程序已经变得越来越复杂了:

  • JS需要管理的状态越来越多,越来越复杂
  • 这些状态包括服务器返回的数据、缓存数据、用户操作产生的数据等等
  • 也包括一些UI的状态,比如某些元素是否被选中,是否显示加载动效,当前分页

当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:

  • 多个视图依赖于同一状态
  • 来自不同视图的行为需要变更同一状态

我们是否可以通过组件数据的传递来完成呢?

  • 对于一些简单的状态,确实可以通过props的传递或者Provide的方式来共享状态
  • 但是对于复杂的状态管理来说,显然单纯通过传递和共享的方式是不足以解决问题的,比如兄弟组件如何共享数据呢?

    Vuex的状态管理

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

  • 状态之间相互会存在依赖,一个状态的变化会引起另一个状态的变化,View页面也有可能会引起状态的变化

  • 当应用程序复杂时,state在什么时候,因为什么原因而发生了变化,发生了什么样的变化,会变得非常难以控制和追踪

因此,我们是否可以考虑将组件的内部状态抽离出来,以一个全局单例的方式来管理呢?

  • 在这种模式下,我们的组件树构成了一个巨大的“视图View”
  • 不管在树的那个位置,任何组件都能获取状态或者触发行为
  • 通过定义和隔离状态管理中的各个概念,并通过强制性的规则来维护视图和状态间的独立性,我们的代码便会变得更加结构化和易于维护、跟踪

这就是Vuex背后的基本思想,它借鉴了Flux,Redux、Elm:
Vuex - 图2

Vuex的基本使用

首先,我们需要在项目的根目录下新建一个store文件夹,用来存放vuex相关的文件。
image.png
然后新建一个index文件,用于使用vuex。

import { createStore } from "vuex";

const store = createStore({
  state() {
    return {
      counter: 0,
    };
  },
  mutations: {
    increment(state) {
      state.counter++;
    },
    decrement(state) {
      state.counter--;
    },
  },
});

export default store;

上面的代码就是index文件中的内容,我们从vuex中引入createSotre函数,然后调用这个函数并将返回值赋给store,最后导出store。
crateSotre函数中我们传入一个对象,对象中包含state和mutations,state就是存放数据的地方,mutations是操作数据的地方,因为案例没有使用异步函数,所以还不需要写actions。

<template>
<h2>{{ $store.state.counter }}</h2>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
</template>

<script>
  export default {
    name: "App",
    data() {
      return {};
    },
    methods: {
      increment() {
        this.$store.commit("increment");
      },
      decrement() {
        this.$store.commit("decrement");
      },
    },
  };
</script>

在vue中使用通过$.store.state来获取state中的参数,如果想要修改,不能直接通过$.store.state来对数据进行更改,而是通过this.$store.commit来调用store的mutations中定义的修改数据的方法,从而实现state数据的更改。

辅助函数mapState

我们使用state中的数据需要通过$store.state.xxxx的形式,非常不方便,所以我们可以借助mapState函数简化操作。

<template>
  <div>
    <!-- <h2>Home:{{ counter }}</h2>
    <h2>Home:{{ name }}</h2>
    <h2>Home:{{ age }}</h2>
    <h2>Home:{{ sno }}</h2> -->
    <h2>Home:{{ sCounter }}</h2>
    <h2>Home:{{ sName }}</h2>
  </div>
</template>

<script>
import { mapState } from "vuex";
export default {
  computed: {
    sCounter() {
      return this.$store.state.counter;
    },
    sName() {
      return this.$store.state.name;
    },
    // ...mapState(["counter", "name", "age", "sno"]),
    ...mapState({
      sCounter: (state) => state.counter,
      sName: (state) => state.name,
    }),
  },
  // computed: mapState(),
};
</script>

mapState有两种使用方式,一种是数组使用,就是传入一个数组给mapState函数,数组项要和state中值相等,那么就可以拿到该值。
另一种方式是通过对象,对象中key值是我们自定义的key值,value值通过state=>state.xxx获取。
两种方式都需要解构。

getters的基本使用

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

import { createStore } from "vuex";

const store = createStore({
  state() {
    return {
      books: [
        { name: "深入Vuejs", price: 200, count: 3 },
        { name: "深入Webpack", price: 140, count: 5 },
        { name: "深入React", price: 280, count: 3 },
        { name: "深入Node", price: 180, count: 6 },
      ],
      discount: 0.5,
    };
  },
  getters: {
    totalPrice(state, getters) {
      return (
        state.books.reduce((acc, cur) => {
          return acc + cur.count * cur.price;
        }, 0) * getters.currentDiscount
      );
    },
    currentDiscount(state) {
      return state.discount * 0.9;
    },
  },
});

export default store;

我们在store中创建了getters,在getters中创建了totalPrice方法和currentDiscount方法,getters中的方法可以接受两个参数,一个是state,还有一个是getters,getters用于在方法内部调用其他方法。

<template>
<div>
  <h2>总价值:{{ $store.getters.totalPrice }}</h2>
  </div>
</template>

使用和state差不多,直接从$store上取值。

<template>
  <div>
    <h2>总价值:{{ $store.getters.totalPrice(2) }}</h2>
  </div>
</template>
totalPrice(state, getters) {
  return function (n) {
    console.log(n); // 2
    return (
      state.books.reduce((acc, cur) => {
        return acc + cur.count * cur.price;
      }, 0) * getters.currentDiscount
    );
  };
},

getters的参数

getters有三个参数,分别是:

  1. state:当前的state对象
  2. getters:当前的getters对象
  3. rootState:当前为子模块时,可以通过rootState获取父模块的state值
  4. rootGetters:当前为子模块时,可以通过rootGetters获取父模块的getters

我们可以给getter传递参数,在getter内层可以通过形参接收。

辅助函数mapGetters

作用和mapState一样,都是为了简化从store中取值的操作。

<template>
  <div>
    <div>{{ nameInfo }}</div>
    <div>{{ ageInfo }}</div>
    <div>{{ snoInfo }}</div>
  </div>
</template>

<script>
import { mapGetters } from "vuex";
export default {
  computed: {
    // ...mapGetters(["nameInfo", "ageInfo", "snoInfo"]),
    ...mapGetters({
      nameInfo: "nameInfo",
      ageInfo: "ageInfo",
      snoInfo: "snoInfo",
    }),
  },
};
</script>

数组取值方式和mapState一样,而对象取值则是直接赋值为其在store中的key值即可。

mutations使用细节

<template>
  <div>
    <h2>当前计数:{{ $store.state.counter }}</h2>
    <hr />
    <button @click="$store.commit('increment')">+1</button>
    <button @click="$store.commit('decrement')">-1</button>
    <button @click="$store.commit('incrementN', 10)">10后</button>
    <button @click="addTen">+10</button>
    <button @click="$store.commit('incrementN', 100)">+100后</button>
  </div>
</template>

<script>
export default {
  methods: {
    addTen() {
      this.$store.commit("incrementN", 10);
    },
  },
};
</script>

在使用mutations的过程中,我们可能会传递一些数据给mutatiosn,可以直接在commit时传递。

mutations: {
  increment(state) {
    state.counter++;
  },
  incrementN(state, payload) {
    state.counter += payload;
  },
  decrement(state) {
    state.counter--;
  },
},

incrementN在前面被commit时传递了额外的参数,我们可以通过payload获取(payload是形参不是实参,名字可以自定义),如果传递多个,可以通过对象或数组传递,也可以一个个传递,那么mutations中的方法接收时想要获取传递的参数,也要定义一相同数量的形参来接收。

mapMutations辅助函数

作用和其他两个一样,唯一的区别在于这个不写在computed中,而是写在methods中

<template>
  <div>
    <h2>当前计数:{{ $store.state.counter }}</h2>
    <hr />
    <button @click="increment">+1</button>
    <button @click="decrement">-1</button>
    <button @click="incrementN(10)">+10</button>
    <button @click="addTen">+10</button>
    <button @click="incrementN(100)">+100</button>
  </div>
</template>

<script>
import { mapMutations } from "vuex";
export default {
  methods: {
    addTen() {
      this.incrementN(10);
    },
    // ...mapMutations(["increment", "decrement", "incrementN"]),
    ...mapMutations({
      increment: "increment",
      decrement: "decrement",
      incrementN: "incrementN",
    }),
  },
};
</script>

也同样有数组和对象两种使用方式,对象使用时,key是我们自己赋值的,value直接写和matations中一样的就可以自动匹配上。

actions的基本使用

action类似于mutation,不同在于:

  • Action提交的是mutation,而不是直接变更状态
  • Action可以包含任意异步操作 ```vue
``` 在组件中通过`this.$store.dispatch("xxx")`的形式调用store的actions中定义的方法。 ```javascript import { createStore } from "vuex"; import axios from "axios"; const store = createStore({ state() { return { banners: [], }; }, actions: { addBannerData(context) { axios.get("http://123.207.32.32:8000/home/multidata").then((res) => { context.commit("addBannerData", res.data.data.banner.list); }); }, }, mutations: { addBannerData(state, payload) { state.banners = payload; }, }, }); export default store; ``` 在store的actions中实现异步方法,但是如果想要操作state中的数据,则actions中必须通过commit调用mutations中定义的方法实现state数据的更改。 actions还支持另一种调用方式: ```javascript // this.$store.dispatch("addBannerData"); this.$store.dispatch({ type: "addBannerData", }); ``` ## actions的参数 actions一共有6个参数:
![image.png](https://cdn.nlark.com/yuque/0/2022/png/422931/1645876403246-3cfcfcba-e67f-405a-88be-cd057b3f93a3.png#clientId=u75371319-8614-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=174&id=u353a2d77&margin=%5Bobject%20Object%5D&name=image.png&originHeight=174&originWidth=755&originalType=binary&ratio=1&rotation=0&showTitle=false&size=27019&status=done&style=none&taskId=ua3fdddd8-8384-4dc5-8767-e2374c87d3f&title=&width=755)
打印actions的context属性,可以看到在这里可以访问很多属性,比如通过commit可以调用mutations中的方法,也可以通过dispatch调用其他的actions方法,也可以使用getters获取getters中的方法。 1. commit:用来调用mutations中的方法 2. dispatch:用来调用其他的actions方法 3. getters:用来获取getters中的方法 4. rootState:当前为子模块时,可以通过rootState获取父模块的state值 5. rootGetters:当前为子模块时,可以通过rootGetters获取父模块的getters 6. state:用来获取当前的state值 ## mapActions辅助函数 和其他的辅助函数用法一致,也支持数组和对象两种方法,如果想要使用别名就需要使用对象方法。 ```vue ``` ## actions的异步操作 actions通常是异步的,那么我们如何知道actions什么时候结束呢? - 我们可以通过让actions返回Promise,在Promise的then中来处理完成后的操作 ```javascript actions: { addBannerData(context) { return new Promise((resolve, reject) => { axios .get("http://123.207.32.32:8000/home/multidata") .then((res) => { context.commit("addBannerData", res.data.data.banner.list); resolve("请求成功了"); }) .catch((err) => { reject(err); }); }); }, }, ``` 首先,我们需要在actions中方法定义的时候返回一个promise ```javascript mounted() { const promise = this.$store.dispatch("addBannerData"); promise .then((res) => { console.log(res); }) .catch((err) => console.log(err)); }, ``` 然后在vue中接收返回的promise,在then和catch里写处理函数。 # module的基本使用 什么是module? - 由于使用单一状态树,应用的所有状态会集中到一个比较大的对象,当应用变得非常复杂时,store对象就有可能变得相当臃肿 - 为了解决以上问题,Vuex允许我们将store分割成块(module) - 每个模块拥有自己的state、mutation、action、getter、甚至是嵌套子模块 首先,我们在store下新建文件夹modules,用来存放各个模块的代码
![image.png](https://cdn.nlark.com/yuque/0/2022/png/422931/1645883131296-2ac82d35-1da1-4805-a436-a8dc99e1f991.png#clientId=u75371319-8614-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=158&id=u86d035c8&margin=%5Bobject%20Object%5D&name=image.png&originHeight=158&originWidth=387&originalType=binary&ratio=1&rotation=0&showTitle=false&size=8096&status=done&style=none&taskId=ub0538adb-840f-438e-9601-9f42c77acd8&title=&width=387)
![image.png](https://cdn.nlark.com/yuque/0/2022/png/422931/1645883175152-896445d6-012d-47a1-afeb-e2dbcb0e3d13.png#clientId=u75371319-8614-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=348&id=u5b6706d4&margin=%5Bobject%20Object%5D&name=image.png&originHeight=423&originWidth=395&originalType=binary&ratio=1&rotation=0&showTitle=false&size=29964&status=done&style=none&taskId=u4ad2ba5a-67a6-4086-bb3f-ce0f6750f7b&title=&width=325)![image.png](https://cdn.nlark.com/yuque/0/2022/png/422931/1645883181830-d9189157-8d4a-419d-8218-0c844c6264f7.png#clientId=u75371319-8614-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=348&id=u2b222a43&margin=%5Bobject%20Object%5D&name=image.png&originHeight=425&originWidth=391&originalType=binary&ratio=1&rotation=0&showTitle=false&size=29610&status=done&style=none&taskId=u6675aa8f-76f4-4191-a028-562badb305e&title=&width=320)
单个模块其实可以看做是一个单独的store ```javascript import { createStore } from "vuex"; import home from "../stroe/modules/home"; import user from "../stroe/modules/user"; const store = createStore({ state() { return { rootCounter: 0, }; }, mutations: { increment(state) { state.rootCounter++; }, }, modules: { home, user, }, }); export default store; ``` 在store文件夹下的index文件中我们将各个模块的文件导入进来,然后在modules中注册他们 ```vue ``` 这样在页面中我们就可以通过`$store.state.模块名.xxx`的方式来使用这些数据。 ## vuex的命名空间 当我们通过module的方式使用vuex时,无法通过`$store.getters.home.doubleHomeCounter`的方式来获取home模块的数据,而是只能通过`$store.getters.doubleHomeCounter`的方式来获取,但是这样虽然能拿到,却会有命名冲突的问题,所以需要通过命名空间来解决。 ```javascript const homeModule = { namespaced: true, state() { return { homeCounter: 100, }; }, getters: { doubleHomeCounter(state) { return state.homeCounter * 2; }, }, mutations: { increment(state) { state.homeCounter++; }, }, actions: { incrementAction(context) { context.commit("increment"); }, }, }; export default homeModule; ``` 命名空间非常简单,就是在模块内部通过设置`namespaced: true`来使用,这样在页面中就可以通过`$store.getters["home/doubleHomeCounter"]`这种方式使用home模块下的getters,至于其他模块的mutations则是这样使用`this.$store.commit("home/increment")`,actions也是类似的使用`this.$store.dispatch("home/incrementAction")`。 ## module的辅助函数 module虽然没有自己的辅助函数,但是module开启命名空间后之前的辅助函数都需要更改写法。 ```vue // 写法一 ``` 除了以上两种,还有第三种写法 ```vue

`` 这种写法依赖于createNamespacedHelpers函数,传递模块名称给它,那么它就能获取该模块的mapState, mapGetters, mapMutations, mapActions`方法。