1. 为什么要使用 Vuex?

来自 官方文档 的介绍:

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 也集成到 Vue 的官方调试工具 devtools extension,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。

开发中,多个组件很可能会共享同一个状态(包括状态和数据),而组件和组件之间可能是兄弟关系,也可能是复杂的多层嵌套关系,如果单靠组件通信完成状态变更和同步,事情会变得很麻烦:

但是有了 Vuex 后,我们可以用它保管公共的状态,每当组件想要访问或者修改共享状态的时候,直接与 Vuex 进行交互就可以,而组件与一个“状态容器”的交互,比起前面的通信是要简单很多的:

3. 安装和使用

安装和 vue-router 是差不多的:

npm install vuex --save

之后,项目的 src 文件夹下会多出 store 文件夹。里面存放的 store.js 可以配置 Vuex(没有的话可以手动创建),大概结构是这样的:

// store.js

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

const store = new Vuex.Store({
    state:{...},
    getters:{...},
    mutations:{...},
    actions:{...},
    modules:{...}         
})

export default store

接着,在 main.js 中引入 store.js,并在 Vue 根实例下注册。

3. Vuex 的核心

Vuex 的核心包括 state、getters、mutations、actions、modules。

这几者的关系,我们可以先看来自官网的图:

3.1. state

Vuex 中的 state 相当于组件中的 data 属性。

store.js:

const store = new Vuex.Store({
    state:{
        name:'Sam'
    }
})

App.vue:

<div>{{$store.state.name}}</div>   <!--Sam-->
<hello-vuex></hello-vuex>   <!--Sam-->

<script>
    import HelloVuex from './components/HelloVuex'
    export default{
        components:{
            HelloVuex
        }
    }
</script>

HelloVuex.vue:

<div>{{$store.state.name}}</div>   

<script>
    export default{

    }
</script>

3.3. getters

Vuex 中的 getters 相当于组件中的 computed 属性。getters 里面的方法接受两个参数,一个是 state ,我们通过它拿到 state 里的数据进行修改;一个是 getters。

const store = new Vuex.Store({
    state:{
        name:'Sam'
    },
    getters:{
        change(state){
            return state.name + 'and Jack'
        }
    }
})
<div>{{$store.getters.change}}</div>  <!--Sam and Jack-->

有时候,我们可能需要在外面传参,但是 getters 里面的方法只接受两个参数,这时候可以考虑让函数返回一个闭包 :

const store = new Vuex.Store({
    state:{
        name:'Sam'
    },
    getters:{
        change(state){
            return function(string){
                return state.name + string
            }
        }
    }
})
<div>{{$store.getters.change('and Tom')}}</div>  <!--Sam and Tom-->

3.3. mutations

Vuex 中的 mutations 相当于组件中的 methods 属性,要更改 Vuex 中的数据,唯一方法就是提交 mutation。这里要注意,mutations 只负责处理同步的事件。

mutations 里面的方法接受两个参数,一个是 state,一个是从外面传进来的 payload(载荷),这个 payload 具体是什么,要看 commit 的风格。

下面的操作会把两个组件中的 “Sam” 都同步修改为 “Jack”:

const store = new Vuex.Store({
    state:{
        name:'Sam'
    },
    mutations:{
        change(state,payload){
            state.name = payload
        }
    }
})
<button @click="changeName">点击修改名字</button>
<div>{{$store.state.name}}</div>  
<hello-vuex></hello-vuex>  

<script>
import HelloVuex from './components/HelloVuex' 
export default {
    components:{
        HelloVuex
    },
    methods:{
        changeName(){
            this.$store.commit('change','Jack')
        }
    }
}
</script>

关于提交风格。我们可以在 commit 的时候直接传入一个对象:

<script>
export default {
    methods:{
        changeName(){
            this.$store.commit({
                type:'change',
                newName:'Jack'
            })
        }
    }
}
</script>

此时,我们接受的 payload 是提交时的这一整个对象,所以对应代码修改为:

const store = new Vuex.Store({
    state:{
        name:'Sam'
    },
    mutations:{
        change(state,payload){
            state.name = payload.newName
        }
    }
})

上面的修改是响应式的,视图和 devtools 中都能看到发生了相应的同步改变。但要注意,下面的代码不会发生响应式的改变:

const store = new Vuex.Store({
    state:{
        obj:{
            name:'Sam'
        }
    },
    mutations:{
        change(state,payload){
            state.obj.age = 24
        }
    }
})

这是因为,我们根本就没有在 obj 上初始化 age 这个属性。要做到响应式,可以修改为:

const store = new Vuex.Store({
    state:{
        obj:{
            name:'Sam'
        }
    },
    mutations:{
        change(state,payload){
            Vue.set(state.obj,age,24)
        }
    }
})

3.4. actions

mutations 中的方法必须是同步的,这样 devtools 才可以进行追踪,依次捕获状态前后的快照;而异步的方法是无法追踪的,因为并不清楚回调函数的执行时机。因此,异步的方法要写在 actions 中。下面的操作可以在 2s 后将 Sam 修改为 Jack:

// store.js

const store = new Vuex.Store({
    state:{
        name:'Sam'
    },
    mutations:{
        change(state,payload){
            state.name = payload
        }
    },
    actions:{
       _change(context,payload){
           setTimeout(() => {
               context.commit('change',payload)
           },2000)
       } 
    }
})
<!-- App.vue-->

<button @click="changeName">点击修改名字</button>    
<div>{{$store.state.name}}</div> 
<hello-vuex></hello-vuex>

<script>
    import HelloVuex from './components/HelloVuex' 
    export default {
        components:{
            HelloVuex
        },
        methods:{
            changeName(){
                this.$store.dispatch('_change','Jack')  // 也接受 payload 参数 
            }
        }
    }
</script>

因为是异步操作,所以我们很可能需要一个回调函数告知我们异步操作的结果,这时候可以用 Promise。也就是,将 actions 中的异步操作包裹在 Promise 中,并返回这个 Promise,之后在 dispatch 后调用 then(因为 dispatch 后返回的是一个 Promise 实例)。代码如下:

// store.js

const store = new Vuex.Store({
    state:{
        name:'Sam'
    },
    mutations:{
        change(state,payload){
            state.name = payload
        }
    },
    actions:{
       _change(context,payload){
           return new Promise((resolve,reject) => {
               setTimeout(() => {
                  context.commit('change',payload)
                  resolve()
               },2000)
           })
       } 
    }
})
<!--App.vue-->

<button @click="changeName">点击修改名字</button>
<div>{{$store.state.name}}</div>
<hello-vuex></hello-vuex>

<script>
    import HelloVuex from './components/HelloVuex' 
    export default {
        components:{
            HelloVuex
        },
        methods:{
            changeName(){
                this.$store
                  .dispatch('_change','Sam')
                  .then(() => {
                    console.log('异步操作成功')
                })	 
            }
        }
    }
</script>

3.5. modules

由于使用单一状态树(单一数据源),应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。

为了解决以上问题,Vuex 允许我们将 store 分割成若干个 module,每个 module 可以看作一个小的单元,它们各自都拥有自己的 state、mutations、actions、getters(甚至是 modules)。

1. state

单个 module(假定是a)最终还是会挂载到 store 的 state 上的:

所以实际上仍然还是单一状态树。可以通过 $store.state.a 拿到这个 module,再拿到它的 state。

// store.js

const moduleA = {
    state:{ 
        age:20 
    }
}
const store = new Vuex.Store({
    state:{
        name:'Sam'
    },
    getters:{},
    mutations:{},
    actions:{},
    modules:{
        a: moduleA
    }
})
<!--App.vue-->

<div>{{$store.state.a.age}}</div>    <!--20-->
2. getters

用法和之前一致,首先会到 store 的 getters 下找对应方法,找不到再去单个 module 下的 getters 寻找。getters 中的方法接受的参数,一个是当前 module 下的 state,一个是当前 module 下的 getters

3. mutations

用法与之前一致,commit 的时候首先会到 store 的 mutations 下找对应方法,找不到再去单个 module 下的 mutations 寻找。

4. actions

用法和之前类似,依然是直接 dispatch,不过 actions 中的方法的参数 context 不再指向 store ,而是指向单个 module。

4. 类型常量

mutations 中的方法名实际上是一个字符串,也就是说,下面两种写法是等效的:

mutations:{
    change(){
        .....
    },
    // 等价于
    ['change'](){
        .....
    }
}

可以将所有的方法名各自用一个常量表示,并将它们写在一个统一的文件中,之后在 mutations 和 commit 中表示方法名字的时候,都用这个常量表示。

5. 辅助函数

每次访问 Vuex 中的数据或者调用方法,都需要使用 this.$store,因此 Vuex 提供了相关的辅助函数,使写法更加简洁。

<tamplate>
    <!--这里可以直接访问 age -->
	<span>{{age}}</span>
    <!--这里可以直接访问 _age -->
	<span>{{_age}}</span>
</tamplate>
<script>
    import {
        mapState,
        mapGetters,
        mapMutations,
        mapActions
    } from 'vuex'
	export default {
        computed: {
            // 将 this.$store.state.age 映射为 this.age
            ...mapState(['age','name']),
            // 将 this.$store.getters._age 映射为 this._age
            ...mapGetters(['_age','_name']),           
        },
        methods: {
             // 将 this.$store.commit('change') 映射为 this.change
            ...mapMutations(['change']),
            // 将 this.$store.dispatch('_change') 映射为 this._change
            ...mapActions(['_change']),
            testMapMutations(){
                // 映射成功,相当于调用了 this.$store.commit('change',12)
                this.change(12)
            },
            testMapActions(){
                // 映射成功,相当于调用了 this.$store.dispatch('_change',14)
                this._change(14)
            }
        }
    }
</script>

PS:如果想要映射成一个不一样的名字,可以给辅助函数传入对象而不是数组。

6. 项目结构

如果把所有的 mutations、getters、actions 等都放在 store.js 文件中,代码会变得很臃肿,所以我们可以进行分离。state 仍然保留在 store.js 文件中,actions、getters、mutations 分离到对应的文件中,各个 module 分离到 modules 文件夹对应的文件中。

import moduleA from './modules/moduleA'
import getters from './getters'
import actions from './actions'
import mutations from './mutations'

const store = new Vuex.Store({
    state:{...},
    getters,
    actions,
    mutations,
    modules:{
      a:moduleA
    }
})