代码改变世界

理解vuex的状态管理模式架构

2017-12-24 16:57  龙恩0707  阅读(3966)  评论(3编辑  收藏  举报

理解vuex的状态管理模式架构

一: 什么是vuex?
官方解释如下:
vuex是一个专为vue.js应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证以一种可预测的方式发生变化。
使用方式有如下2种:
1. 如果直接在浏览器下引用包的话;如下:

<script src="https://unpkg.com/vue@2.5.13/dist/vue.js"></script>
<script src="https://unpkg.com/vuex@3.0.1/dist/vuex.js"></script>

接下来就可以使用了。

2. 使用npm安装
npm install vuex --save
然后在入口文件引入方式如下:

import Vue from 'vue';
import Vuex from 'vuex';
// vuex
Vue.use(Vuex);

首先我们先来看看一个简单的demo,再来对比下vuex到底做了什么事情。具体为我们解决了什么事情?
我们先来实现一个简单的demo,有一个标签显示数字,两个按钮分别做数字的加一和减一的操作;如下使用纯vue的demo如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>vue-demo</title>
    <script src="https://unpkg.com/vue/dist/vue.js"></script>
  </head>
  <body>
    <div id="app">
      <p>{{count}}
        <button @click="inc">+</button>
        <button @click="dec">-</button>
      </p>
    </div>
    <script>
      new Vue({
        el:'#app',
        data () {
          return {
            count: 0
          }
        },
        methods: {
          inc () {
            this.count++
          },
          dec () {
            this.count--
          }
        }
      })
    </script>
  </body>
</html>

如上的代码的含义是:button的标签内绑定两个函数,当点击的时候 分别调用 inc 和 dec的对应的函数,接着会调用 vue中的methods的对应的方法
。然后会对data中的count属性值发生改变,改变后会把最新值渲染到视图中。

注意:上面的代码直接复制运行下就可以看到效果了。

现在我们来看看使用vuex的方式来实现如上demo。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>vue-demo</title>
    <script src="https://unpkg.com/vue/dist/vue.js"></script>
    <script src="https://unpkg.com/vuex@3.0.1/dist/vuex.js"></script>
  </head>
  <body>
    <div id="app">
      <p>{{count}}
        <button @click="inc">+</button>
        <button @click="dec">-</button>
      </p>
    </div>
    <script>
      const store = new Vuex.Store({
        state: {
          count: 0
        },
        mutations: {
          inc: state => state.count++,
          dec: state => state.count--
        }
      });
      const app = new Vue({
        el: '#app',
        computed: {
          count() {
            return store.state.count;
          }
        },
        methods: {
          inc() {
            store.commit('inc');
          },
          dec() {
            store.commit('dec');
          }
        }
      });
    </script>
  </body>
</html>

注意:上面的代码直接复制运行下就可以看到效果了。

对比下上面的代码:
1. 引用vuex源码;
2. methods的方法不变,但是方法内的逻辑不在函数内进行,而是让store对象去处理。
3. count数据不再是一个data函数返回的对象的属性了。而是通过store方法内的计算字段返回的。
具体的调用如下:
先view上的元素操作点击事件 -> 调用methods中的对应方法 -> 通过store.commit(type) 触发store中的mutations对应的方法来改变state的属性,值发生改变后,视图就得到更新。
回到store对象上来,store对象是 Vuex.Store的实列。在store内分为state对象和mutations对象,其中state存放的是状态,
比如count属性就是它的状态值,而mutations则是一个会引发状态改变的所有方法。

理解什么是状态管理模式?
状态管理:简单的理解就是统一管理和维护各个vue组件的可变化状态。

我们明白vue是单向数据流的,那么它的状态管理一般包含如下几部分:
1. state; 驱动应用的数据(一般指data中返回的数据)。
2. view; 一般指模板,以声明的方式将state的数据映射到视图。
3. actions: 响应在view上的用户输入导致的状态变化
但是当我们的应用遇到多个组件共享状态时候,那么单向数据流可能不太满足我们的需求:
比如如下几个方面:
1. 多个视图依赖于同一状态。
传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。

2. 我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。
因此我们可以把组件的共享状态提取出来,作为全局来管理,因此vuex产生了。

vuex的优点:
最主要解决了组件之间共享同一状态的问题。可以把组件的共享状态提取出来,作为全局来管理。

什么情况下我应该使用 Vuex?

如果不打算开发大型单页应用,使用 Vuex 可能是繁琐冗余的。确实是如此——如果您的应用够简单,最好不要使用 Vuex。一个简单的 global event bus 就足够您所需了。但是,如果您需要构建是一个中大型单页应用,很可能会考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择。

二: Vuex状态管理的demo学习
每一个Vuex应用的核心就是store(仓库), store是保存应用中大部分的状态。
Vuex 和一般的全局对象有以下几点不同:
1. Vuex的状态存储是响应性的。
   当vue组件从store中读取状态的时候,若store中的状态发生变化,那么相对应的组件也就会得到相应的更新。
2. 我们不能直接修改store中的状态。
   改变store中的状态的唯一途径是显示地提交(commit)mutations.

2-1 单一状态树
Vuex使用的是单一状态树,用一个对象就包含了全部的应用层级状态。这也意味着每个应用将仅仅包含一个store的实列。
Vuex的状态存储是响应性的,因此从store实列中读取一个状态的最简单的方法是在计算属性返回某个状态。
比如demo2的代码:

<div id="app">
  <p>{{count}}
    <button @click="inc">+</button>
    <button @click="dec">-</button>
  </p>
</div>
<script>
  const store = new Vuex.Store({
    state: {
      count: 0
    },
    mutations: {
      inc: state => state.count++,
      dec: state => state.count--
    }
  });
  const app = new Vue({
    el: '#app',
    computed: {
      count() {
        return store.state.count;
      }
    },
    methods: {
      inc() {
        store.commit('inc');
      },
      dec() {
        store.commit('dec');
      }
    }
  });
</script>

如上代码,从store中读取一个状态可以从 computed中的count方法内就可以读取到了。当 store.state.count变化的时候,都会重新求取计算属性,并且触发相关联的DOM更新。
但是这种模式导致组件依赖的全局状态单列,在模块构建系统中,在每个需要使用state的组件中需要频繁的导入(因为每个页面都需要
导入 new Vuex.Store这样的,但是一个应用系统仅仅包含一个store实列),并且在测试组件的时候需要模拟状态。
因此vuex通过store选项,提供了一种机制将状态从根组件注入到每一个子组件中。

使用vuex的示列:

new Vuex.Store({
  state: {
    // code 
  },
  mutations: {
    // code ....
  }
});

state是用来存储初始化的数据的。如果要读取数据使用 $store.state.数据变量。
修改数据使用mutations,它保存的需要改变数据的所有方法,改变mutations里的数据需要使用 $store.commit();

还是需要 vue-cli 中的项目来说明下:
1. 在scr目录下新建一个vuex文件夹,在该文件夹下 新建 mystore.js文件,代码如下:

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

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    count: 1
  },
  // 需要修改stats的数据的话,需要使用$store.commit()方法
  mutations: {
    add(state) {
      return state.count++;
    },
    reduce(state) {
      return state.count--;
    }
  }
});

2. 在src/views文件夹下 新建一个 count.vue,代码如下:

<template>
  <div>
    <p>{{ msg }}
    <!-- 获取vuex文件的mystore.js中的 state中的count的值 -->
    {{ $store.state.count }}
    </p>
    <p>
      <button @click="$store.commit('add')"> + </button>
      <button @click="$store.commit('reduce')"> - </button>
    </p>
  </div>
</template>

<script>
  import mystore from '@/vuex/mystore';
  export default {
    data () {
      return {
        msg: 'Hello world'
      }
    },
    /*
     引用mystore.js,store为数据仓库
     */
    store: mystore
  }
</script>

3、在 src/router/index.js 路由配置文件中配置 count.vue 的路由, 代码如下:

import Vue from 'vue';
import Router from 'vue-router';
// import HelloWorld from '@/views/HelloWorld';

Vue.use(Router);

const router = new Router({
  mode: 'history', // 访问路径不带井号  需要使用 history模式,才能使用 scrollBehavior
  routes: [
    {
      path: '/count',
      name: 'count',
      component: resolve => require(['@/views/count'], resolve) // 使用懒加载
    }
  ]
});
export default router;

直接在浏览器访问 http://localhost:8080/count 即可了。

三:学习Vuex state访问状态对象
上面我们的代码是访问状态对象的,是单页应用程序中的共享值,现在我们再来看看状态对象如何赋值给内部对象,也就是
把mystore.js中的值,赋值给模板里data的值,也就是想直接在template中用 {{xxx}}直接调用数据。

我们知道vuex的状态存储是响应性的,从store实列中读取状态最简单的方式是在计算属性中返回某个状态。

3-1 通过computed的计算属性直接赋值
在src/views文件夹下新建 count2.vue, 代码如下:

<template>
  <div>
    <p>{{ msg }}
    <!-- 获取vuex文件的mystore.js中的 state中的count的值 -->
    {{ $store.state.count }}
    </p>
    <p>computed计算赋值结果是:{{ count }}</p>
    <p>
      <button @click="$store.commit('add')"> + </button>
      <button @click="$store.commit('reduce')"> - </button>
    </p>
  </div>
</template>

<script>
  import mystore from '@/vuex/mystore';
  export default {
    data () {
      return {
        msg: 'Hello world'
      }
    },
    computed: {
      count () {
        return this.$store.state.count
      }
    },
    /*
     引用mystore.js,store为数据仓库
     */
    store: mystore
  }
</script>

在浏览中访问 http://localhost:8080/count2 可以看到。

3-2 通过mapState的对象来赋值
当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余,为了解决这个问题,可以使用
mapState辅助函数来帮助我们生成计算属性。

在src/views文件夹下新建 count3.vue, 代码如下:

<template>
  <div>
    <p>{{ msg }}
    <!-- 获取vuex文件的mystore.js中的 state中的count的值 -->
    {{ $store.state.count }}
    </p>
    <p>computed计算赋值结果是:{{ count }}</p>
    <p>
      <button @click="$store.commit('add')"> + </button>
      <button @click="$store.commit('reduce')"> - </button>
    </p>
  </div>
</template>

<script>
  import mystore from '@/vuex/mystore';
  // 引入mapState
  import { mapState } from 'vuex';

  export default {
    data () {
      return {
        msg: 'Hello world'
      }
    },
    computed: mapState({
      count: function(state) {
        return state.count;
      }
    }),
    /*
     引用mystore.js,store为数据仓库
     */
    store: mystore
  }
</script>

在浏览中访问 http://localhost:8080/count3 可以看到。

3-3 通过mapState的数组来赋值,
在src/views 下 新建 count4.vue, 代码如下:

<template>
  <div>
    <p>{{ msg }}
    <!-- 获取vuex文件的mystore.js中的 state中的count的值 -->
    {{ $store.state.count }}
    </p>
    <p>computed计算赋值结果是:{{ count }}</p>
    <p>
      <button @click="$store.commit('add')"> + </button>
      <button @click="$store.commit('reduce')"> - </button>
    </p>
  </div>
</template>

<script>
  import mystore from '@/vuex/mystore';
  // 引入mapState
  import { mapState } from 'vuex';

  export default {
    data () {
      return {
        msg: 'Hello world'
      }
    },
    /*
     * 数组中的count 必须和 mystore.js定义的常量 mystate 中的 count同名,
     因为这是直接访问mystate的count
    */
    computed: mapState(['count']),
    /*
     引用mystore.js,store为数据仓库
     */
    store: mystore
  }
</script>

在浏览中访问 http://localhost:8080/count4 可以看到。

四: getters计算过滤操作
    有时候我们需要从store中的state中派生出一些状态,比如在使用store中的state之前,我们会对state中的某些字段进行过滤一下,比如对state中的count字段都进行加10这样的数据;但是如果有多个组件需要用到这个操作的话,那么我们就需要复制这个函数,或者抽取到一个共享函数内,
然后多处导入这个函数,但是这上面两种方式都不是太好,因为我们现在有更好的方式来解决它。
Vuex中允许我们在store中定义 getters,getters的返回值会根据它的依赖被缓存起来,且只有当他的依赖值发生改变了
才会重新计算。
现在我们需要对mystore.js文件中的count进行一个计算属性的操作,在它输出之前,加上10的操作。
如下代码有两个按钮,一个加5,一个减5,那么在加5或者减5之前,先加20,然后再进行加5或者5操作。代码如下:
在文件夹 src/vuex/mystore.js代码如下:

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

Vue.use(Vuex);

// 增加一个常量对象 state
const mystate = {
  count: 0
};
// mutations 保存所有的方法,该方法可以改变state数据
const mymutations = {
  // 增加
  add(state, num) {
    const count = state.count += num;
    return count;
  },
  // 减少
  reduce(state, num) {
    const count = state.count -= num;
    return count;
  }
};

// 增加一个getters对象
const mygetters = {
  mycount: function(state) {
    const count = state.count += 20;
    return count;
  }
};

// 封装代码,让外部可见
export default new Vuex.Store({
  state: mystate,  // state的固定写法 保存数据的状态值
  mutations: mymutations, // mutations的固定写法 改变数据的所有方法
  getters: mygetters
});

在src/views/下新建 count5.vue 代码如下:

<template>
  <div>
    <p>{{ msg }}
    <!-- 获取vuex文件的mystore.js中的 state中的count的值 -->
    {{ $store.state.count }}
    </p>
    <p>computed计算赋值结果是:{{ mycount }}</p>
    <p>
      <!-- 
        $store.commit('add', 5) 第一个参数是方法名,第二个是参数
      -->
      <button @click="$store.commit('add', 5)"> + </button>
      <button @click="$store.commit('reduce', 5)"> - </button>
    </p>
    <div>
      <p>使用mapMutations修改状态:</p>
      <p>
        <button @click="add(10)">+</button>
        <button @click="reduce(10)">-</button>
      </p>
    </div>
  </div>
</template>

<script>
  import mystore from '@/vuex/mystore';
  // 引入mapState
  import { mapState, mapMutations, mapGetters } from 'vuex';

  export default {
    data () {
      return {
        msg: 'Hello world'
      }
    },
    computed: {
      // mapState(['count']) 此处的count必须和store.js定义的常量 mystate中的count同名,因为这是直接访问 mystate的count
      ...mapState(['count']),
      // mapGetters 辅助函数,可以将store中的getter映射到局部计算属性mycount
      ...mapGetters(['mycount'])
    },
    methods: mapMutations(['add', 'reduce']),
    /*
     引用mystore.js,store为数据仓库
     */
    store: mystore
  }
</script>

在浏览器下 访问 http://localhost:8080/count5 即可。

五:Mutations修改状态
Mutations是修改vuex中的store的唯一方法。每个mutations都有一个字符串的事件类型(type)和一个回调函数(handler)。这个回调函数就是
我们进行更改的地方。它也会接受state作为第一个参数。

打开上面src/vuex/mystore.js 代码中的 mutations 可以看到如下:

// mutations 保存所有的方法,该方法可以改变state数据
const mymutations = {
  // 增加
  add(state, num) {
    const count = state.count += num;
    return count;
  },
  // 减少
  reduce(state, num) {
    const count = state.count -= num;
    return count;
  }
};

我们之前调用 mutations的方法是这样的 $store.commit()即可调用方法来改变state数据,现在我们想使用 @click="add()"来调用。
1. 我们在src/views/ 下新建一个count6.vue, 先导入我们的 mapMutations方法

import { mapState, mapMutations } from 'vuex'; 

2. 使用methods属性,并加入 mapMutations

methods: mapMutations(['add', 'reduce']); 

3. 在template中使用 @click="", 如下代码:

<button @click="add(5)">+</button>
<button @click="reduce(5)">-</button>

count6.vue 代码如下:

<template>
  <div>
    <p>{{ msg }}
    <!-- 获取vuex文件的mystore.js中的 state中的count的值 -->
    {{ $store.state.count }}
    </p>
    <p>computed计算赋值结果是:{{ count }}</p>
    <p>
      <!-- 
        $store.commit('add', 5) 第一个参数是方法名,第二个是参数
      -->
      <button @click="$store.commit('add', 5)"> + </button>
      <button @click="$store.commit('reduce', 5)"> - </button>
    </p>
    <div>
      <p>使用mapMutations修改状态:</p>
      <p>
        <button @click="add(10)">+</button>
        <button @click="reduce(10)">-</button>
      </p>
    </div>
  </div>
</template>

<script>
  import mystore from '@/vuex/mystore';
  // 引入mapState
  import { mapState, mapMutations } from 'vuex';

  export default {
    data () {
      return {
        msg: 'Hello world'
      }
    },
    computed: {
      // mapState(['count']) 此处的count必须和store.js定义的常量 mystate中的count同名,因为这是直接访问 mystate的count
      ...mapState(['count'])
    },
    methods: mapMutations(['add', 'reduce']),
    /*
     引用mystore.js,store为数据仓库
     */
    store: mystore
  }
</script>

src/vuex/mystore.js代码修改如下:

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

Vue.use(Vuex);

// 增加一个常量对象 state
const mystate = {
  count: 0
};
// mutations 保存所有的方法,该方法可以改变state数据
const mymutations = {
  // 增加
  add(state, num) {
    const count = state.count += num;
    return count;
  },
  // 减少
  reduce(state, num) {
    const count = state.count -= num;
    return count;
  }
};

// 封装代码,让外部可见
export default new Vuex.Store({
  state: mystate,  // state的固定写法 保存数据的状态值
  mutations: mymutations // mutations的固定写法 改变数据的所有方法
});

六: actions异步修改状态
actions是异步修改state的状态的。但是Mutations是同步改变状态的。

6-1 在mystore.js中声明actions
actions是可以调用Mutations的方法的。如下代码:

// 增加一个 actions
const myactions = {
  addAction(context) {
    console.log(context);
    context.commit('add', 5); // 调用mymutations 中的 add方法,并传参数5
  },
  reduceAction(context) {
    context.commit('reduce', 5); // 调用mymutations中的reduce方法,并传参数5
  }
};

myactions 里有两个方法 addAction 和 reduceAction , 在方法体内,我们都用 commit 调用了 Mutations
里面的方法。
其中context,是上下文对象,在这边可以理解为store本身。

在Vuex.store()中封装

// 封装代码,让外部可见
export default new Vuex.Store({
  state: mystate,  // state的固定写法 保存数据的状态值
  mutations: mymutations, // mutations的固定写法 改变数据的所有方法
  getters: mygetters,
  actions: myactions
});

在conut7.vue中调用,代码如下:

<p>
  actions的异步操作<br/>
  <button @click="addAction"> + </button>
  <button @click="reduceAction"> - </button>
</p>

import引用如下:

import { mapState, mapMutations, mapGetters, mapActions } from 'vuex';

添加methods方法,代码如下:

methods: {
...mapMutations(['add', 'reduce']),
...mapActions(['addAction', 'reduceAction'])
}

下面是全部代码:

mystore.js代码如下:

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

Vue.use(Vuex);

// 增加一个常量对象 state
const mystate = {
  count: 0
};
// mutations 保存所有的方法,该方法可以改变state数据
const mymutations = {
  // 增加
  add(state, num) {
    const count = state.count += num;
    return count;
  },
  // 减少
  reduce(state, num) {
    const count = state.count -= num;
    return count;
  }
};

// 增加一个getters对象
const mygetters = {
  mycount: function(state) {
    const count = state.count += 20;
    return count;
  }
};
// 增加一个 actions
const myactions = {
  addAction(context) {
    console.log(context);
    context.commit('add', 5); // 调用mymutations 中的 add方法,并传参数5
  },
  reduceAction(context) {
    context.commit('reduce', 5); // 调用mymutations中的reduce方法,并传参数5
  }
};
// 封装代码,让外部可见
export default new Vuex.Store({
  state: mystate,  // state的固定写法 保存数据的状态值
  mutations: mymutations, // mutations的固定写法 改变数据的所有方法
  getters: mygetters,
  actions: myactions
});

count7.vue代码如下:

<template>
  <div>
    <p>{{ msg }}
    <!-- 获取vuex文件的mystore.js中的 state中的count的值 -->
    {{ $store.state.count }}
    </p>
    <p>computed计算赋值结果是:{{ mycount }}</p>
    <p>
      <!-- 
        $store.commit('add', 5) 第一个参数是方法名,第二个是参数
      -->
      <button @click="$store.commit('add', 5)"> + </button>
      <button @click="$store.commit('reduce', 5)"> - </button>
    </p>
    <div>
      <p>使用mapMutations修改状态:</p>
      <p>
        <button @click="add(10)">+</button>
        <button @click="reduce(10)">-</button>
      </p>
      <p>
        actions的异步操作<br/>
        <button @click="addAction"> + </button>
        <button @click="reduceAction"> - </button>
      </p>
    </div>
  </div>
</template>

<script>
  import mystore from '@/vuex/mystore';
  // 引入mapState
  import { mapState, mapMutations, mapGetters, mapActions } from 'vuex';

  export default {
    data () {
      return {
        msg: 'Hello world'
      }
    },
    computed: {
      // mapState(['count']) 此处的count必须和store.js定义的常量 mystate中的count同名,因为这是直接访问 mystate的count
      ...mapState(['count']),
      ...mapGetters(['mycount'])
    },
    methods: {
      ...mapMutations(['add', 'reduce']),
      ...mapActions(['addAction', 'reduceAction'])
    },
    /*
     引用mystore.js,store为数据仓库
     */
    store: mystore
  }
</script>

可以查看github上的代码