Vuex记录

82 阅读4分钟

Vuex官方文档链接

Vuex 是什么?

每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state) 。Vuex 和单纯的全局对象有以下两点不同:

  1. Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
  2. 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。

啥是“状态管理模式”?

以一个vue的单文件组件来举例:

image.png

为啥使用 Vuex?

当我们开发一个大型的单页面应用时,会出现大量的共享数据,可能很多页面会用到同一个数据,维护起来就比较困难。通过使用vuex对这些共享的数据进行集中化管理,可以使我们的数据更清晰,项目更结构化并且好维护。 (数据就是官网文档里状态的意思,我是这么理解的。)

Vuex在项目中的引入和使用

安装

以下是NPM安装方式,其他的可以瞅瞅官网。

// 注意:不标明版本的时候是安装的最新版本,Vue2.x需要使用vuex3.x版本
npm install vuex --save
npm install vuex@3 --save

引入项目

这里演示的是在main.js中引入,正常使用时一般会以单独的文件写。

import Vue from 'vue';
import Vuex from 'vuex'; // 第一步:引入插件

Vue.use(Vuex);  // 第二步:通过全局方法 `Vue.use()` 使用vuex插件

// 第三步:创建一个 store,里边是state对象和mutation...
const store = new Vuex.Store({
  // ......
});

new Vue({
  store,  // 第四步:将状态从根组件“注入”到每一个子组件中
  render: h => h(App),
}).$mount('#app');

Vuex的核心概念

State

State就跟咱们单个组件里的data差不多,不过State是全局的。

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    count: 0,
  },
});

new Vue({
  store,
  render: h => h(App),
}).$mount('#app');

到这一步,你就可以在每个页面中使用state中定义的数据啦!

怎么使用嘞?(state部分就以咱们上边代码里定义的‘count’来举例)

this.$store.state.count;// 这样就能获取到咱们定义的count了

官方文档说需要通过组件中的计算属性(computed)来访问,但我看着直接访问也没多大区别。

<template>
  <div>
    <p>{{ count }}</p>
  </div>
</template>

<script>
export default {
  computed: {
    count () {
      return this.$store.state.count
    }
  },
}
</script>

按照官方文档中获取vuex数据的方式接着往下走:

当一个组件中需要用到多个vuex数据时,还像上边代码里那么写,就很麻烦了!

所以Vuex给咱们提供了一个辅助函数(mapState),它可以帮助我们自动生成计算属性的函数,好处就是少写几行代码:

<template>
  <div>
    <p>{{ count }}</p>
    <p>{{ countAlias }}</p>
    <p>{{ countPlusLocalState }}</p>
  </div>
</template>

<script>
// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex';

export default {
  data() {
    return {
      localCount: '哈哈哈',
    }
  },
  computed: mapState({
    // 箭头函数可使代码更简练
    count: state => state.count,

    // 传字符串参数 'count' 等同于 `state => state.count`
    countAlias: 'count',

    // 为了能够使用 `this` 获取局部状态,必须使用常规函数
    countPlusLocalState (state) {
      return state.count + this.localCount;
    }
  }),
}
</script>

当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState 传一个字符串数组:

computed: mapState([
  'count',
]),

像上边两种情况只考虑了使用Vuex中的数据,但是正常开发中,组件也存在自己的局部计算属性。所以可以使用 对象展开运算符 来混合它们使用。

computed: (
  // 你自己的计算属性该咋写就咋写
  ...mapState({
    // 这里边可以参照上边的代码(对象方式、数组方式)
  })
),

---------- state结束 ----------

Getter

有些时候原始的 Vuex 数据不能满足咱们的需要,得做一些处理后才能使用,比如对一个数组的数据进行条件过滤。但咱们不能改变原始数据啊,所以Vuex允许我们定义一个getters选项,来做这些操作。这玩意就跟咱们组件中的computed差不多的意思。

定义getter

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    userArray: [
      { name: '张三', age: 40 },
      { name: '李四', age: 20 }
    ],
  },
  // 定义getters
  getters: {
    getUserFil: state => {
      return state.userArray.filter(i => i.age < 30);
    }
    // 可以通过让 getter 返回一个函数,实现给 getter 传参。
    getUserFilCopy: (state) => (name) => {
      return state.userArray.filter(i => i.name === name);
    }
  }
});

new Vue({
  store,
  render: h => h(App),
}).$mount('#app');

访问getter

<template>
  <div>
    <p>年龄小于30的人是:{{ userFil[0].name }}</p>
    <p>张三的年龄是:{{ userFilCopy[0].name }}</p>
  </div>
</template>

<script>
export default {
  computed: {
    // 1. 通过属性访问getter
    userFil() {
      return this.$store.getters.getUserFil;
    },
    // 2.通过方法访问getter
    userFilCopy() {
      return this.$store.getters.getUserFilCopy('张三');
    }
  },
}
</script>

getter也有一个辅助函数mapGetters,跟state的差不多,访问多个getter时少写点代码。这玩意目前貌似不允许以方法的形式访问getter,没法传参。

<template>
  <div>
    <p>年龄小于30的人是:{{ userFil[0].name }}</p>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'
export default {
  computed: {
    ...mapGetters({
      'userFil',
    }),
  },
}
</script>

Mutation

更改state中数据的时候,不建议直接去改动,官方文档的说法是:

【你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。】

在实际项目中,就我本人经历的项目哈,其实也会有直接更改的情况出现(当然这是不规范的哈)。

所以嘞,这个时候就需要咱们在mutations选项里,定义一些用来处理state中数据的函数。

定义mutation

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    count: 0,
  },
  mutations: {
    // 不传参数的写法
    increment (state) {
      state.count++
    },
    // 荷载(就是要给mutation传参数进去)
    incrementCopy (state, num) {
      state.count += num;
    },
  }
});

new Vue({
  store,
  render: h => h(App),
}).$mount('#app');

提交mutation

<template>
  <div>
    <p>count:{{ getCount }}</p>
    <div>
      <button @click="toCommitMutation">提交mutation</button>
      <button @click="toCommitMutationCopy">提交mutation(传递参数)</button>
    </div>
  </div>
</template>

<script>
import { mapMutations } from 'vuex'
export default {
  computed: {
    getCount() {
      return this.$store.state.count;
    },
  },
  methods: {
    toCommitMutation() {
      this.$store.commit('increment');
    },
    // 需要传参的情况
    toCommitMutationCopy() {
      this.$store.commit('increment', 30);
    },
  },
}
</script>

官方文档里还有一种风格的提交方式,简单了解下:

store.commit({
  type: 'increment', // 这个type就是咱们定义的mutation函数名
  amount: 10, // 这里是要传递的参数
})

不出意外,mutation也有一个辅助函数:mapMutations

<template>
  <div>
    <p>count:{{ getCount }}</p>
    <div>
      <button @click="toCommitMutation">提交mutation</button>
      <button @click="toCommitMutationCopy">提交mutation(传递参数)</button>
    </div>
  </div>
</template>

<script>
import { mapMutations } from 'vuex';
export default {
  computed: {
    getCount() {
      return this.$store.state.count;
    },
  },
  methods: {
    toCommitMutation() {
      // 以下三行代码是同一个作用!
      // this.$store.commit('increment');
      // this.add();
      this.increment();
    },
    // 需要传参的情况
    toCommitMutationCopy() {
      // 以下三行代码是同一个作用!
      // this.$store.commit('incrementCopy', 30);
      // this.addCopy(30);
      this.incrementCopy(30);
    },
    
    // mapMutations的两种写法
    // 1.(对象形式,需要起别名的话就用这个)
    ...mapMutations({
      add: 'increment', // 将 `this.add()` 映射为 `this.$store.commit('increment')`
      addCopy: 'incrementCopy'
    }),
    // 2.(数组形式,不需要起别名就用这个)
    ...mapMutations([
      'increment',
      'incrementCopy'
    ])
  },
}
</script>

注意:一条重要的原则就是要记住 mutation 必须是同步函数

Action

Action 类似于 mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态。
  • Action 可以包含任意异步操作。

以下例子和说明直接用官方文档。

注册action

const store = createStore({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})

Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。当我们在之后介绍到 Modules 时,你就知道 context 对象为什么不是 store 实例本身了。

实践中,我们会经常用到 ES2015 的参数解构来简化代码(特别是我们需要调用 commit 很多次的时候):

actions: {
  increment ({ commit }) {
    commit('increment')
  }
}

分发 Action

Action 通过 store.dispatch 方法触发:

store.dispatch('increment')

乍一眼看上去感觉多此一举,我们直接分发 mutation 岂不更方便?实际上并非如此,还记得 mutation 必须同步执行这个限制么?Action 就不受约束!我们可以在 action 内部执行异步操作:

actions: {
  incrementAsync ({ commit }) {
    setTimeout(() => {
      commit('increment')
    }, 1000)
  }
}

Actions 支持同样的载荷方式和对象方式进行分发:

// 以载荷形式分发
store.dispatch('incrementAsync', {
  amount: 10
})

// 以对象形式分发
store.dispatch({
  type: 'incrementAsync',
  amount: 10
})

来看一个更加实际的购物车示例,涉及到调用异步 API 和分发多重 mutation

actions: {
  checkout ({ commit, state }, products) {
    // 把当前购物车的物品备份起来
    const savedCartItems = [...state.cart.added]
    // 发出结账请求
    // 然后乐观地清空购物车
    commit(types.CHECKOUT_REQUEST)
    // 购物 API 接受一个成功回调和一个失败回调
    shop.buyProducts(
      products,
      // 成功操作
      () => commit(types.CHECKOUT_SUCCESS),
      // 失败操作
      () => commit(types.CHECKOUT_FAILURE, savedCartItems)
    )
  }
}

注意我们正在进行一系列的异步操作,并且通过提交 mutation 来记录 action 产生的副作用(即状态变更)。

在组件中分发 Action

你在组件中使用 this.$store.dispatch('xxx') 分发 action,或者使用 mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用(需要先在根节点注入 store):

import { mapActions } from 'vuex'

export default {
  // ...
  methods: {
    ...mapActions([
      'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`

      // `mapActions` 也支持载荷:
      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
    ]),
    ...mapActions({
      add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
    })
  }
}

组合 Action

Action 通常是异步的,那么如何知道 action 什么时候结束呢?更重要的是,我们如何才能组合多个 action,以处理更加复杂的异步流程?

首先,你需要明白 store.dispatch 可以处理被触发的 action 的处理函数返回的 Promise,并且 store.dispatch 仍旧返回 Promise:

actions: {
  actionA ({ commit }) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        commit('someMutation')
        resolve()
      }, 1000)
    })
  }
}

现在你可以:

store.dispatch('actionA').then(() => {
  // ...
})

在另外一个 action 中也可以:

actions: {
  // ...
  actionB ({ dispatch, commit }) {
    return dispatch('actionA').then(() => {
      commit('someOtherMutation')
    })
  }
}

最后,如果我们利用 async / await,我们可以如下组合 action:

// 假设 getData() 和 getOtherData() 返回的是 Promise

actions: {
  async actionA ({ commit }) {
    commit('gotData', await getData())
  },
  async actionB ({ dispatch, commit }) {
    await dispatch('actionA') // 等待 actionA 完成
    commit('gotOtherData', await getOtherData())
  }
}

一个 store.dispatch 在不同模块中可以触发多个 action 函数。在这种情况下,只有当所有触发函数完成后,返回的 Promise 才会执行。

Module

如果我们需要维护的数据量特别大,且存在许多getter、mutation、action,这时候store对象的代码量就很臃肿了,看着也不舒服,不利于维护。

为了解决这个问题,Vuex允许我们将store切割成多个模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:

const moduleA = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... }
}

const store = createStore({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态

除非你的项目复杂到一定程度,才会出现分模块的情况。

一个简单的store文件结构(不划分模块)

(下边不写action,如果需要异步操作,写法是一样的。)

├── components
│   ├── xxx.vue
│   └── ...
├── view
│   ├── xxx.vue
│   └── ...
├── App.vue
├── main.js
└── store
    ├── index.js          # 导出 store 的地方
    ├── state.js          # 根级别的 state
    ├── getters.js        # 根级别的 getter
    └── mutations.js      # 根级别的 mutation

a: 将之前咱们写在main.js中的代码抽离出来:(可以在src目录下创建一个store文件夹)

import Vue from 'vue';
import Vuex from 'vuex';

import state from './state';
import getters from './getters';
import mutations from './mutations';

Vue.use(Vuex);

const store = new Vuex.Store({
  state,
  getters,
  mutations,
});

export default store;
const state = {
  count: 0,
  userArray: [
    { name: '张三', age: 40 },
    { name: '李四', age: 20 }
  ],
};

export default state;
const getters = {
  getFileData: (state) => {
    return state.userList.filter(i => i.age > 20);
  },
  getFileDataCopy: (state) => (name) => {
    return state.userList.filter(i => i.name === name);
  }
};

export default getters;
const mutations = {
  increment (state) {
    state.count ++; 
  },
  incrementCopy (state, num) {
    state.count += num;
  },
};

export default mutations;

b. 修改main.js文件

import Vue from 'vue';
import store from './store/index.js'; // vuex

new Vue({
  store,
  render: h => h(App),
}).$mount('#app');

一个简单的分模块store就实现了!