继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

Vue的状态管理器:Vuex

2018-09-20 16:25:314379浏览

Sunday

6实战 · 14手记 · 9推荐
TA的实战

声明:本章内容属于本人发布到GitChat平台中《深入浅出学习Vue开发》中的部分内容,在争取了发布平台的同意之后,才发布于慕课网。


这一章我们来学习Vuex,如果要学习好Vuex那么最最重要的就是要知道Vuex是干嘛的,我们为什么需要Vuex。因为对于Vuex来说,它的使用方式非常简单,知识点也不多。Vuex唯一的难点就是很多人无法理解它。所以在本章我们会着重的讲解Vuex的作用,我们为什么需要它,在理解了这些之后,我们再去学习Vuex的使用就会水到渠成了。

状态管理

我们直接来看这一段代码:

<div id="app">
    <com-1></com-1>
    <com-2></com-2>
</div>

<script type="text/x-template" id="com-1">
    <div>
        <input type="button" @click="addCount" value="count++">
    </div>
</script>

<script type="text/x-template" id="com-2">
    <div>
        count: {{count}}
    </div>
</script>

<script>

    Vue.component('com-1', {
        template: '#com-1',
        data: function () {
            return {
                count: 0
            }
        },
        methods: {
            addCount: function () {
                this.count += 1;
            }
        }
    });

    Vue.component('com-2', {
        template: '#com-2',
        data: function () {
            return {
                count: 0
            }
        },
    });


    var vm = new Vue({
        el: '#app',

    });
</script>

在这段代码中,我们期望能够通过点击com-1中的count++按钮来改变com-2中的count,使其每点击一下自加一。基于这个需求我们应该如何去实现呢?基于我们现在所学到的知识,一共有两种解决办法,我们来看一下。

第一种解决方案就是:通过组件传参的方式。我们知道在Vue中,兄弟组件是没有办法进行传参操作的,那么按照我们现在所学到的知识,如果希望通过组件间传参的方式解决这个问题,那么就需要对我们的html结构进行一下修改,我们看下面的代码:

<div id="app">
    <com-1></com-1>  
</div>

<script type="text/x-template" id="com-1">
    <div>
        <input type="button" @click="addCount" value="count++">
        <com-2 v-bind:count="count"></com-2>
    </div>
</script>

<script type="text/x-template" id="com-2">
    <div>
        count: {{count}}
    </div>
</script>

<script>
    Vue.component('com-1', {
        template: '#com-1',
        data: function () {
            return {
                count: 0
            }
        },
        methods: {
            addCount: function () {
                this.count += 1;
            }
        }
    });

    Vue.component('com-2', {
        template: '#com-2',
        props: {
            count: 0
        }
    });

    var vm = new Vue({
        el: '#app'
    });
</script>

在上面的代码中我们把com-2变成了com-1的一个子组件,当我们点击count++按钮的时候,我们通过propscom-2中传递了count的值,我们在学习组件的时候学习过 当我们使用prop进行参数传递的时候,父组件数据的改变会影响子组件 。这样,我们在com-1count的变化就会在com-2中被展示出来。

在我们的Demo事例中,通过这种方法来维护count的状态未尝不可。不过大家想一下,在我们的实际项目中各个组件的层级会变得非常复杂,同时对于这种数据的状态管理如果都通过这种组件之间传参的方式来进行解决的话,则会变得非常难以维护。那么我们就会想有没有其他的方式可以解决这个问题?那就是采用全局状态管理的方法。

我们看一下下面的代码:

<div id="app">
    <com-1></com-1>
    <com-2></com-2>
</div>

<script type="text/x-template" id="com-1">
    <div>
        <input type="button" @click="addCount" value="count++">
    </div>
</script>

<script type="text/x-template" id="com-2">
    <div>
        count: {{count}}
    </div>
</script>

<script>
    var store = {
        count: 0
    };

    Vue.component('com-1', {
        template: '#com-1',
        data: function () {
            return store
        },
        methods: {
            addCount: function () {
                this.count += 1;
            }
        }
    });

    Vue.component('com-2', {
        template: '#com-2',
        data: function () {
            return store
        },
    });

    var vm = new Vue({
        el: '#app',
    });
</script>

我们看到在上面的代码中我们维护了一个store对象,用作两个组件中原始数据对象的实际来源 当访问数据对象时,一个 Vue 实例(组件实例)只是简单的代理访问。 所以,如果你有一处需要被多个实例间共享的状态,可以简单地通过维护一份数据来实现共享。现在当 store 发生变化,com-1com-2 都将自动的更新引用它们的视图。从而达到了我们需求,当我们在com-1中点击count++的时候在com-2中展示count的值。

但是对我们现在的代码来说,依然存在一个非常致命的问题,那就是 在任何时间,我们应用中的任何部分,在任何数据改变后,都不会留下变更过的记录。这在我们想要进行代码调试的时候,将会变成一个噩梦。所以我们可以采用一个简单的store模式来解决这个问题:

<div id="app">
    <com-1></com-1>
    <com-2></com-2>
</div>

<script type="text/x-template" id="com-1">
    <div>
        <input type="button" @click="addCount" value="count++">
    </div>
</script>

<script type="text/x-template" id="com-2">
    <div>
        count: {{sharedState.count}}
    </div>
</script>

<script>
    var store = {
        debug: true,
        // 数据 / 状态
        state: {
            count: 0
        },
        // state的变化要通过mutation来进行
        mutation: {
            addCount: function (state) {
                if (store.debug) console.log('调用addCount方法,count自加1');
                state.count += 1;
            }
        }
    };

    Vue.component('com-1', {
        template: '#com-1',
        data: function () {
            return {
                // 私有数据
                privateState: {

                },
                // 共同数据
                sharedState: store.state
            }
        },
        methods: {
            addCount: function () {
                // 调用
                store.mutation.addCount(store.state);
            }
        }
    });

    Vue.component('com-2', {
        template: '#com-2',
        data: function () {
            return {
                // 私有数据
                privateState: {

                },
                // 共同数据
                sharedState: store.state
            }
        },
    });

    var vm = new Vue({
        el: '#app',
    });
</script>

在上面的代码中我们为store增加了debug、state、mutation三个属性,statestore对象中所有状态数据的描述,当state中的数据想要发生改变的时候都要通过mutation来进行。比如我们的count++的点击事件,则是通过调用store.mutation.addCount方法进行,以此来记录数据的改变。

这样的一种方式在Vue中被称为**store模式**,我们使用**store模式作为我们整个项目的状态管理器**,但是当我们的项目变得越来越大,越来越复杂的时候,我们更需要一个更加专业,更加全面的状态管理器,那么这个状态管理器就是Vuex

Vuex

Vuex继承了store模式的思想,并做了更多的扩展,提供了更多的功能。我们先来看一下Vuex的定义:

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。致力于管理项目中所有公用数据的状态。

我们参照上面的几个例子,是不是对Vuex的定义更加容易理解。Vuex是在store模式上的扩展,它们拥有相同的思想。我们看一下Vuex中的核心功能。

1、State :Vuex中的 “ 唯一数据源 ”
2、Getter :Vuex 中的计算属性,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
3、Mutation :更改Vuex中数据的唯一方式,就是通过mutation进行修改,只支持同步操作。就像我们上面的例子一样。
4、Action :当我们需要异步更改数据时,通过 Action 提交的是 mutation,而不是直接变更状态。
5、Module : 由于Vuex使用单一状态树,应用的所有状态会集中到一个比较大的对象。Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter甚至是嵌套子模块——从上至下进行同样方式的分割。

由前面的例子我们也可以看出,我们不希望state中的状态被直接改变(虽然我们可以这么做),而是期望把Mutation作为改变state的唯一方式,而当我们必须要通过异步来改变状态的时候,我们期望使用action来提交mutaion用于改变状态。大家思考一下Vuex为什么要这么去设计呢?

Vuex中说白了,任何的操作都是围绕state来进行的,Vuex状态管理器,作用就是管理state中的状态,其他提供的所有功能Getter、Mutation、Action都是为了能够更好的管理state,而之所以设计成期望通过Mutation改变状态,是因为我们期望**所有状态的变化都是有迹可循的!**我们通过一个图示来看一下Vuex的设计流程(图示来自Vuex官网):

这里写图片描述

图示完美的解释了Vuex的执行流程:我们在Vue的组件中提交Dispatch操作,用以操作对应的Actions方法,然后在Actions的方法中,通过提交Commit操作调用对应的Mutation方法来修改对应的状态State,最后State的改变被渲染到我们的组件中。这就是Vuex的整个核心流程,我们通过一段代码来描述一下:

<div id="app">
    <com-1></com-1>
    <com-2></com-2>
</div>

<script type="text/x-template" id="com-1">
    <div>
        <input type="button" @click="addCount" value="count++">
    </div>
</script>

<script type="text/x-template" id="com-2">
    <div>
        count: {{this.$store.state.count}}
    </div>
</script>

<script>
    /**
     * 声明一个Vuex实例,这个实例全局只有一个
     * */
    const store = new Vuex.Store({
        state: {
            count: 1
        },
        mutations: {
            /**
             * Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 
             * 和 一个 回调函数 (handler)。
             * 这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:
             * */
            increment (state, num) {
                // 变更状态
                state.count += num;
            }
        },
        actions: {
            /**
             * Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,
             * 因此你可以调用 context.commit 提交一个 mutation,
             * 或者通过 context.state 和 context.getters 来获取 state 和 getters。
             * 但是 context 对象并!不!等!于!store 实例本身
             * */
            increment (context, num) {
                // 延迟1秒提交commit操作
                setTimeout(function () {
                    /**
                     * 你不能直接调用一个 mutation handler。
                     * 这个选项更像是事件注册:“当触发一个类型为 increment 的 mutation 时,调用此函数。
                     * ”要唤醒一个 mutation handler,你需要以相应的 type 调用 store.commit 方法.
                     * 你可以向 store.commit 传入额外的参数,即 mutation 的 载荷(payload)。
                     * 或者我们也可以通过 对象风格的提交方式:
                     * store.commit({
                            type: 'increment',
                            num: 1
                        })
                       这种情况下我们将在 mutation 中接收到一个对象
                     * */
                    context.commit('increment', num)
                }, 1000);
            }
        }
    })

    Vue.component('com-1', {
        template: '#com-1',
        methods: {
            addCount: function () {
                /**
                 * 我们可以直接向 store.dispatch 传入额外的参数,即 action 的 载荷(payload)。
                 * 或者我们也可以通过 对象风格的提交方式:
                 * store.dispatch({
                        type: 'increment',
                        num: 1
                    })
                    这种情况下我们将在 action 中接收到一个对象
                 * */
                this.$store.dispatch('increment', 1);
            }
        }
    });

    Vue.component('com-2', {
        template: '#com-2',
    });

    var vm = new Vue({
        el: '#app',
        store , // 等同于 store:store
    });
</script>

在上面的代码中,我们首先通过new Vuex.Store去声明了一个Vuex的实例store ,当我们点击count++按钮的时候,调用this.$store.dispatch('increment', 1);其实是调用了Vuexactions中定义的increment方法,在这个方法中我们声明了一个setTimeout,指定在1秒钟之后执行context.commit('increment', num)方法,调用mutationincrement方法,然后我们在mutationincrement方法中修改了state中的count的值,使其state.count += num;。这样的一个执行流程就是我们Vuex中所提倡的执行方法,也是我们上方图示中所描述的执行流程。

我们知道state、mutaion、action是整个Vuex最核心的内容,在它们之外,Getter、Module也是我们必须要了解的内容。我们看下面的代码:

...
<script type="text/x-template" id="com-2">
   <div>
       <!-- 我们将要通过模块的路径来调整 state的命名 -->
       <p>count: {{this.$store.state.a.count}}</p>
       <!-- Getter 会暴露为 store.getters 对象,你可以以属性的形式访问这些值: -->
       <p>doubleCount: {{this.$store.getters.doubleCount}}</p>
   </div>
</script>

<script>
   /**
    * 声明一个Vuex模块
    * 默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的
    * ——这样使得多个模块能够对同一 mutation 或 action 作出响应。
    * 而 state 则是模块的局部状态对象!
    * 
    *    如果希望你的模块具有更高的封装度和复用性,
    *    你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。
    *    当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。
    * */
   const moduleA = {
       // namespaced: true,
       state: {
           count: 1
       },
       getters: {
          /**
            * Getter 接受两个参数,
            * 第一为:state 状态对象
            * 第二位:其他的 getter 
            * */
           doubleCount: function (state, getters) {
               console.log('getters.count: ' + getters.count);
               return state.count * 2;
           },
           count: function (state) {
               return state.count;
           },
       },
       mutations: {
           increment (state, num) {
               // 这里的 `state` 对象是模块的局部状态
               state.count += num;
           }
       },
       ...
   };

   /**
    * 声明一个Vuex实例,这个实例全局只有一个
    * */
   const store = new Vuex.Store({
       modules: {
           a: moduleA
       }
   });
   ...
</script>

在上面的代码中,我们只贴出了关键性代码,我们把Vuex进行了模块化的区分,提供了moduleA用以描述state、getters、mutations等,具体的作用在上面的代码中有详细的注释,我们这里就不在过多叙述了。

总结

在本章的内容中,我们并没有一头扎进Vuex的细节中,因为对Vuex来说,它的具体使用方式比较容易,只要我们能够理解它的本质作用,它的执行流程那么在我们使用Vuex的时候就会水到渠成。

除去我们讲到的Vuex的核心功能之外,Vuex还提供了一些其他的功能,比如plugins插件,这些内容在Vuex官网中介绍的非常清楚,我们这里就不想在进行重复介绍了。

···································

欢迎关注课程:

打开App,阅读手记
1人推荐
发表评论
随时随地看视频慕课网APP