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
}
})