【Vue】Vuex 从使用到原理分析(上篇)

前言

在使用 Vue 开发的过程当中,一旦项目到达必定的体量或者模块化粒度小一点,就会遇到兄弟组件传参,全局数据缓存等等问题。这些问题也有其余方法能够解决,可是经过 Vuex 来管理这些数据是最好不过的了。html

该系列分为上中下三篇:vue

1. 什么是 Vuex ?

Vuex 是一种状态管理模式,它集中管理应用内的全部组件状态,并在变化时可追踪、可预测。vuex

也能够理解成一个数据仓库,仓库里数据的变更都按照某种严格的规则。vue-cli

国际惯例,上张看不懂的图。慢慢看完下面的内容,相信这张图再也不那么难以理解。api

vuex

状态管理模式

在 vue 单文件组件内,一般咱们会定义 datatemplatemethods 等。数组

  • data 一般做为数据源保存咱们定义的数据缓存

  • template 做为视图映射咱们 data 中的数据app

  • methods 则会响应咱们一些操做来改变数据异步

这样就构成了一个简单的状态管理。async

2. 为何要使用 Vuex?

不少状况下,咱们使用上面举例的状态自管理应用也能知足场景了。可是如前言里所说,全局数据缓存(例如省市区的数据),兄弟组件数据响应(例如单页下 Side 组件和 Header 组件参数传递)就会破坏单向数据流,而破坏的代价是很大的,轻则“卧槽,这是谁写的不可回收垃圾,噢,是我!”,重则都没法理清逻辑来重构。

Vuex 的出现则解决了这一难题,咱们不须要知道数据具体在哪使用,只须要去通知数据改变,而后在须要使用到的地方去使用就能够了。

3. 需不须要使用 Vuex?

首先要肯定本身的需求是否是有那么大...

若是肯定数据寥寥无几,那使用一个 store 模式来管理就能够了,杀鸡不用宰牛刀。

下面用一个全局计数器来举例。

store.js&main.js

// store.js
export default {
  state: {
    count: 0,
  },
  // 计数增长
  increaseCount() {
    this.state.count += 1;
  },

  // 计数归零
  resetCount() {
    this.state.count = 0;
  },
};

// main.js
import store form './store';
Vue.prototype.$store = store;
复制代码

App.vue

<template>
  <div id="app">{{ state.count }}</div>
</template>

<script>
export default {
  name: 'App',
  data() {
    return {
      state: this.$store.state,
    };
  },
  mounted() {
    // 2 秒后计数加 1,视图会变化
    setTimeout(() => {
      this.$store.increaseCount();
    }, 2000);
  },
};
</script>
复制代码

像这样就完成了一个简单的全局状态管理,可是,这样 state 中的数据不是响应式的,这里是经过绑定到了 data 下的 state 中达到响应的目的,当咱们须要用到共享的数据是实时响应且能引起视图更新的时候该如何作呢?

4. 如何使用 Vuex?

既然知道了本身要使用 Vuex,那么如何正确地使用也是一门学问。

4.1 Vuex 核心概念简介

Vuex的核心无外 StateGetterMutationActionModule 五个,下面一个个来介绍他们的做用和编写方式。

State

Vuex的惟一数据源,咱们想使用的数据都定义在此处,惟一数据源确保咱们的数据按照咱们想要的方式去变更,能够经过 store.state 来取得内部数据。

Getter

store 的计算属性,当咱们须要对 state 的数据进行一些处理的时候,能够先在 getters 里进行操做,处理完的数据能够经过 store.getters 来获取。

Mutation

Vuex让咱们放心使用的地方也就是在这,store 里的数据只容许经过 mutation 来修改,避免咱们在使用过程当中覆盖 state 形成数据丢失。

若是咱们直接经过 store.state 来修改数据,vue 会抛出警告,并没有法触发 mutation 的修改。

image-20190911172051077

每个 mutation 都有一个 type 和 callback 与之对应,能够经过 store.commit(type[, payload]) 来提交一个 mutation 到 store,修改咱们的数据。

ps: payload 为额外参数,能够用来修改 state。

Action

在 Mutation 中并不容许使用异步操做,当咱们有异步操做(例如 http 请求)时,就必须在 Action 中来完成了。

Action 中提交的是 mutation,不是直接变动数据,因此也是容许的操做。

咱们能够经过 store.dispatch(action) 来触发事件,进行相关操做后,经过 commit 方法来提交 mutation。

Module

Vuex 的管理已经很美好了,可是!全局数据还好,反正不少地方会要用到,若是只是某个单页下的兄弟组件共享某些数据呢,那这样大张旗鼓地放到Vuex中,长此以往便会臃肿没法管理,故 Module 就是解决这个问题。

每一个 Module 就是一个小型的 store,与上面几个概念一模一样,惟一不一样的是 Module 支持命名空间来更好的管理模块化后的 store。

强烈建议每个模块都写上。

namespaced: true

设置这个属性后,咱们就须要经过 store.state.moduleName 获取 Module 下的 state 里的数据,而 commit 和 dispatch 须要在传递 type 的时候加入路径(也就是模块名)store.commit('moduleName/type', payload)。

4.2 入门版食用方式

咱们如下所说都是以 vue-cli 脚手架生成的项目为例来介绍几种常规写法。

当咱们的须要共享的数据量很小时,只须要简单的写在 store.js 文件里就能够了,并且不须要使用到 Module,使用方式也比较简单。

store.js

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

Vue.use(Vuex);
const vm = new Vue();

export default new Vuex.Store({
  state: {
    count: 0,
  },
  mutations: {
    setCount(state, count) {
      state.count = count;
    },
  },
  getters: {
    getCountPlusOne(state) {
      return state.count + 1;
    },
  },
  actions: {
    setCountAsync({ commit }, count) {
      return new Promise(async resolve => {
        const { data: count } = await vm.$http('/api/example', { count });
        commit('setCount', count);
        resolve(count);
      });
    },
  },
});
复制代码

main.js

// main.js
import Vue from 'vue';
import App from './App';
import store from './store';

Vue.config.productionTip = false;

/* eslint-disable no-new */
new Vue({
  el: '#app',
  store,
  template: '<App />',
  components: { App },
});
复制代码

那么咱们在组件中须要用到时就能够经过如下操做得到想要的结果了:

  • this.$store.state.count:实时获取 count 的值
  • this.$store.getters.getCountPlusOne:获取 count + 1 的值并缓存
  • this.$store.getters.getCountPlusOne():每次都会计算一个新的结果
  • this.$store.commit('setCount', 5):实时改变 count 的值
  • this.$store.dispatch('setCountAsync', 3):派发事件,异步更新 count 的值

在 commit 和 dispatch 的时候,若是须要传入多个参数,可使用对象的方式。

e.g.

this.$store.commit('setCount', { 
  count: 5,
  other: 3 
}); 
// 或者以下
thit.$store.commit({ 
  type: 'setCount',
  count: 5,
  other: 3,
});
复制代码

4.3 进阶版食用方式

通常来讲,入门版食用方式真的不太推荐,由于项目写着写着就和你的身材同样,一每天不忍直视。故须要进阶版来优化优化咱们的“数据仓库”了。

总的来讲,核心概念仍是那样,只是咱们将 store 按照核心概念进行拆分,并将一些常数固定起来,避免拼写错误这种弱智出现,也方便哪天须要修改。

整个项目结构以下:

store/

├── actions.js

├── getters.js

├── index.js

├── mutation-types.js

├── mutations.js

└── state.js

咱们依旧以上面的例子为例来改写:

mutation-types.js

这个文件也是强烈建议编写的,将 mutation 的方法名以常量的方式定义在此处,在其余地方经过 import * as types from './mutation-types'; 来使用,第一能够避免手抖拼写错误,第二能够方便哪天须要改动变量名,改动一处便可。

export const SET_COUNT = 'SET_COUNT';
复制代码

state.js

export default {
  count: 0,
}
复制代码

getters.js

getCountPlusOne(state) {
  return state.count + 1;
};
复制代码

actions.js

import Vue from 'vue';
import * as types from './mutation-types';

const vm = new Vue();

export const setCountAsync = ({ commit }, count) => {
  return new Promise(async resolve => {
    const { data: count } = await vm.$http('/api/example', { count });
    commit('setCount', count);
    resolve(count);
  });
};
复制代码

mutations.js

import * as types from './mutation-types';

export default {
  [types.SET_COUNT](state, count) {
    state.count = count;
  },
};
复制代码

index.js

import Vue from 'vue';
import Vuex from 'vuex';
import createLogger from 'vuex/dist/logger';
import * as actions from './actions';
import * as getters from './getters';
import state from './state';
import mutations from './mutations';
import * as types from './mutation-types';

Vue.use(Vuex);

const debug = process.env.NODE_ENV !== 'production';

const logger = createLogger(); // 引入日志,帮助咱们更好地追踪 mutaion 触发的 state 变化

export default new Vuex.Store({
  actions,
  getters,
  state,
  mutations,
  strict: debug,
  plugins: debug ? [logger] : [],
});
复制代码

在项目中咱们能够经过 this.$store 来获取数据或者提交 mutation 等。同时也可使用辅助函数来帮助咱们更加便捷的操做 store,这部份内容放到[高级版食用方式](#4.4 高级版食用方式)里介绍。

至此,咱们编写了一个完成进阶版的食用方式,大多数项目经过这样的结构来管理 store 也不会让一个 store.js 文件成百上千行了。

可是,又是可是。我有些数据仅仅在小范围内使用,写在这个里面,体量一多不仍是会找不到北吗?

那请继续看下面的高级版食用方式。

4.4 高级版食用方式

所谓的高级版食用方式也就是在进阶版的基础上将大大小小的数据分解开来,让他们找到本身的归宿。

关键的概念就是 Module了,经过这个属性,咱们能够将数据仓库拆解成一个个小的数据仓库来管理,从而解决数据冗杂问题。

这里的写法有两种,一种是将 module 写到 store 中进行统一管理;另外一种则是写在模块处,我我的喜欢第二种,就近原则。

// store/
// + └── modules.js 

// src/helloWorld/store.js
export const types = {
  SET_COUNT: 'SET_COUNT',
};

export const namespace = 'helloWorld';

export default {
  name: namespace, // 这个属性不属于 Vuex,可是经过常量定义成模块名,避免文件间耦合字符串
  namespaced: true, // 开启命名空间,方便判断数据属于哪一个模块
  state: {
    count: 0,
  },
  mutations: {
    [types.SET_COUNT](state, count) {
      state.count = count;
    },
  },
  getters: {
    getCountPlusOne(state) {
      return state.count + 1;
    },
  },
};


// store/modules.js
import helloWorld from '@/views/helloWorld/store';
export default {
  [helloWorld.name]: helloWorld,
}

// store/index.js
+ import modules from './modules';
export default new Vuex.Store({
+  modules,
});
复制代码

因为 module 里的数据很少,因此咱们写在一个 store 文件内更为方便,固然了,若是你的数据够多,这里继续拆分也是能够的。 高级版食用方式通常这样也差很少了,下面补充几个注意点。

module store 的操做方式

与全局模式差很少,只多了在获取使用到命名空间。

  • 提交 Mutation:this.$store.commit('helloWorld/SET_COUNT', 1);
  • 提交 Action:this.$store.dispatch('helloWorld/SET_COUNT', 2);
  • 获取 Gtters:this.$store.getters['helloWorld/getCountPlusOne'];
  • 获取 State:this.$store.state.helloWorld.count;

在 module 内获取总仓库的状态

// store.js
// ...
export default {
  // ...
  getters: {
    /** * state、getters:当前模块的 state、getters * rootState、rootGetters:总仓库的状态 */
    getTotalCount(state, getters, rootState, rootGetters) {
      return state.count + rootState.count;
    },
  },
  actions: {
    // 同上注释
    setTotalCount({ dispatch, commit, getters, state, rootGetters, rootState}) {
      const totalCount = state.count + rootState.count;
      commit('SET_COUNT', totalCount);
    },
  },
}
复制代码

这样一来,咱们一个健壮的Vuex共享数据仓库就建造完毕了,下面会介绍一些便捷的操做方法,也就是Vuex提供的辅助函数。

在子 module 内派发总仓库的事件

有时候咱们须要在子模块内派发一些全局的事件,那么能够经过分发 action 或者提交 mutation 的时候,将 { root: true } 做为第三个参数传递便可。

// src/helloWorld/store.js
export default {
  // ...
  actions: {
    setCount({ commit, dispatch }) {
      commit('setCount', 1, { root: true });
      dispatch('setCountAsync', 2, { root: true });
    },
  },
};
复制代码

4.5 辅助函数

当咱们须要频繁使用 this.$stoe.xxx 时,就总是须要写这么长一串,并且 this.$store.commit('aaaaa/bbbbb/ccccc', params) 也很是的不优雅。Vuex 提供给了咱们一些辅助函数来让咱们写出更清晰明朗的代码。

mapState

import { mapState } from 'vuex';

export default {
  // ...
  computed: {
    // 参数是数组
    ...mapState([
      'count', // 映射 this.count 为 this.$store.state.count
    ]),
    // 取模块内的数据
    ...mapState('helloWorld', {
      localCount: 'count', // 映射 this.localCount 为 this.$store.state.helloWorld.count
    }),
    // 少见写法
    ...mapState({
      count: (state, getters) => state.count,
    }),
  },
  created() {
    console.log(this.count); // 输出 this.$store.state.count 的数据
  },
};
复制代码

mapGetters

使用f方式与 mapState 如出一辙,毕竟只是对 state 作了一点点操做而已。

mapMutation

必须是同步函数,不然 mutation 没法正确触发回调函数。

import { mapMutations } from 'vuex';

export default {
  // ...
  methods: {
    ...mapMutaions([
      'setCount', // 映射 this.setCount() 映射为 this.$store.commit('setCount');
    ]),
    // 带命名空间
    ...mapMutations('helloWorld', {
      setCountLocal: 'setCount', // 映射 this.setCountlocal() 为 this.$store.commit('helloWorld/setCount');
    }),
    // 少见写法
    ...mapMutaions({
      setCount: (commit, args) => commit('setCount', args),
    }),
  },
};
复制代码

mapAction

与 mapMutation 的使用方式如出一辙,毕竟只是对 mutation 作了一点点操做而已, 少见写法里 commit 换成了 dispatch。

4.6 其余补充

动态模块

模块动态注册功能使得其余 Vue 插件能够经过在 store 中附加新模块的方式来使用 Vuex 管理状态

试想有一个通知组件,属于外挂组件,须要用到Vuex来管理数据,那么咱们能够这样作:

  • 注册:this.$store.registerModule(moduleName)
  • 销毁:this.$store.unregisterModule(moduleName)

store.js:

export const namespace = 'notice';
export const store = {
  namespaced: true,
  state: {
    count: 1,
  },
  mutations: {
    setCount(state, count) {
      state.count += count;
    },
  },
};
复制代码

Notice.vue:

<template>
  <div>
    {{ count }}
    <button @click="handleBtnClick">add count</button>
  </div>
</template>

<script>
import { store, namespace } from './store';

export default {
  name: 'Notice',
  computed: {
    count() {
      return this.$store.state[namespace].count;
    },
  },
  beforeCreate() {
    // 注册 notice 模块
    this.$store.registerModule(namespace, store);
  },
  methods: {
    handleBtnClick() {
      this.$store.commit(`${namespace}/setCount`, 1);
    },
  },
//  beforeDestroy() {
//    // 销毁 notice 模块
//    this.$store.unregisterModule(namespace);
//  },
}  
</script>
复制代码

模块重用

有时候须要建立一个模块的多个实例,那 state 可能会形成混乱。咱们能够相似 vue 中 data 的作法,将 state 写成函数返回对象的方式:

export default {
// old state
// state: {
// count: 0,
// },
  
// new state
  state() {
    return {
      count: 0,
    };
  },
};
复制代码

createNamespacedHelpers

在模块有命名空间的时候,咱们在使用数据或者派发事件的时候须要在常量前加上命名空间的值,有些时候写起来也不是很舒服,Vuex提供了一个辅助方法createNamespacedHelpers,能帮助咱们直接生成带命名空间的辅助函数。

// old
import { mapState } from 'vuex';

export default {
  // ...
  computed: {
    ...mapState('helloWorld', [
      'count',
    ]);
  },
};


// use createNamespacedHelpers function
import { createNamespacedHelpers } from 'vuex';
const { mapState } = createNamespacedHelpers('helloWorld');

export default {
  // ...
  computed: {
    ...mapState([
      'count',
    ]);
  },
};
复制代码

插件

在上面咱们已经使用到了一个vuex/dist/logger插件,他能够帮助咱们追踪到 mutaion 的每一次变化,而且在控制台打出,相似下图。

image-20190916154825437

能够清晰地看到变化前和变化后的数据,进行对比。

插件还会暴露 mutaion 钩子,能够在插件内提交 mutaion 来修改数据。

更多神奇的操做能够参考官网慢慢研究,这里不是重点不作更多介绍(实际上是我想象不到要怎么用)。

严格模式

const store = new Vuex.Store({
  // ...
  strict: true
})
复制代码

当开启严格模式,只要 state 变化不禁 mutaion 触发,则会抛出错误,方便追踪。

生产环境请关闭,避免性能损失。

能够经过构建工具来帮助咱们process.env.NODE_ENV !== 'production'

表单处理

当在表单中经过v-model使用Vuex数据时,会有一些意外状况发生,由于用户的修改并非由 mutaion 触发,因此解决的问题是:使用带有setter的双向绑定计算属性。

// template
<input v-model="message">
  
// script
export default {
  // ...
  computed: {
    message: {
      get () {
        return this.$store.state.obj.message
      },
      set (value) {
        this.$store.commit('updateMessage', value)
      },
    },
  },
};
复制代码

总结

经过上面的一些例子,咱们知道了如何来正确又优雅地管理咱们的数据,如何快乐地编写Vuex。回到开头,若是你尚未理解那张图的话,不妨再把这个过程多看一下,而后再看看Vuex 从使用到原理分析(中篇)更深刻地了解Vuex

相关文章
相关标签/搜索