一、什么是vuex
在一个复杂的大型系统中,状态
会在多个组件之间跨层级地、错综复杂地传递,这会使得状态难以追踪,debug起来也会很麻烦。而vuex
就是为了解决这么一个问题而出现的东西,它能够集中地管理应用的状态,并且能够使得每一种状态都是以可预测的方式发生变化的。
二、单向数据流
可以从一个简单的例子切入:
new Vue({
data() {
return {
count: 0
}
},
template: `<div>{{count}}</div>`,
methods: {
increment() {
this.count++
}
}
})
从例子里,可以发现该应用的状态是自管理的
,且它包含有以下的部分:
state
:驱动应用的数据源view
:以声明方式将state
映射到视图action
:响应在view
上的用户输入导致的状态变化
单向数据流图示如:
但是大型的系统中,往往会面临以下的问题:
1)多个组件之间需要依赖于同一个状态
2)来自不同组件的行为,需要更新同一个状态
在一般情况下,如果我们要解决问题1,那么我们就需要把所依赖的这个状态一层一层地传递下去。而如果需要解决问题2,那么就需要父子组件的直接引用,或者通过事件来变更和同步状态。而这些解决问题的办法通常会带来令人困扰的维护难题。
而vuex
的核心思想是提供一个全局的单例统一进行管理状态,并且定义和隔离状态管理中的各种状态,强制要求遵循约定来编写代码,而这带来的好处便是代码会更加的结构化和容易维护。
三、核心概念
1、仓库(store)
store
是一个容器,用来容纳应用中的绝大部分的状态(通常是共享状态)。它和全局对象的区别在于:
1)状态存储是响应式的,可以实现数据和视图的双向绑定
2)不能够直接改变store里的状态,而是应该显示地提交mutations。这样子做的好处就是我们能够方便地追踪变更
创建一个store
的方法如:
// 如果是在模块化系统中使用,开头需要先调用 Vue.use(Vuex)
const store = Vuex.Store({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++
}
}
})
此后,当我们需要用到这个状态的时候,可以使用store.state
来调用,如:
store.state.count
当需要改变这个状态的时候,则使用
store.commit('increment')
2、状态(State)
Vuex使用单一状态树,用一个对象包含了全部的应用层级状态。vuex中只有一个唯一的数据源,也就是只有一个store
实例。
一般情况下,我们通过以下方式能够获得store里的状态:
const Counter = {
template: `<div>{{count}}</div>`,
computed: {
count() {
return store.state.count
}
}
}
但是这种方式个缺陷,因为store.state.count
中的store
其实是个全局变量,在每个组件中使用的时候,我们就需要频繁地导入这个单例。为了使得store的导入更为方便,vuex中的解决方案为:将store
注入到跟组件中,然后每一个子组件中都自动获得this.$store
来引用store单例,如:
const app = new Vue({
store, // 在根组件里注入
components: {
Counter
},
template: `
<div>
<counter></counter>
</div>
`
})
此后,Counter
里就可以这么写:
const Counter = {
template: `<div>{{count}}</div>`,
computed: {
count() {
return this.$store.state.count;
}
}
}
由于当一个组件需要多个状态的时候,每次都声明一个计算属性,这个过程就有点麻烦。为了解决这个问题,vuex提供了mapState
辅助函数,使用如:
import { mapState } from 'vuex';
export default {
// ...
computed: mapState({
/*
相当于:count(state) {
return state.count
}
*/
count: state => state.count,
/*
'count' 相当于 state => state.count
*/
countAlias: 'count',
})
}
当计算属性的名称和state中状态名称相同的时候,可以给mapState
传一个字符串数组,如:
computed: mapState([
'count',
'name'
])
它的效果相当于:
computed: {
count(state) {
return state.count;
},
name(state) {
return state.name;
}
}
如果我们的组件有局部的计算属性,那么如何混合使用呢?因为mapState()
返回的是一个对象,所以我们可以使用...
展开运算符来实现,如:
computed: {
localComputed() { /* code */ },
...mapState({
// code
})
}
注意:虽然vuex可以很方便地管理状态,但这并不意味着所有的状态都需要放到vuex里,放到vuex里的状态,一般是需要在不同组件之间传递和使用的。如果一个状态是严格只属于自身的,那么可以不必放到Vuex里
3、Getters
getters
相当于store里的计算属性,它用来从state中派生出新的状态。可以在Vuex.Store的选项里添加如:
const store = Vuex.Store({
// ...
getters: {
countPlus1(state) {
return state.count + 1;
}
}
// ...
});
getters
里的属性和Vue的计算属性一样,是具有缓存的,只有当依赖上的值发生了变更,相应的值才会更新。当我们在组件里需要用到这个getter的时候,可以使用this.$store.getter
对象来获取,一种方便的使用方式是在组件内定义计算属性,如:
computed: {
countPlus1() {
return this.$store.state.count + 1;
}
}
但是如果我们需要多个getter的时候,这样子写无疑是麻烦的。为了解决这个问题,和state一样,vuex也提供了mapGetters
方法,像上述例子就可以改写为:
computed: {
...mapGetters([
'countPlus1'
])
}
如果我们在组件内使用的getter名字和store里不一样的话,那么可以使用对象的形式,如:
computed: {
...mapGetters({
myCount: 'countPlus1'
})
}
4、Mutations
在vuex里面,我们不能够直接改变状态,而是应该通过提交mutation。mutations应该是一个同步函数,其中不应该包含有异步操作(如果需要用到异步,请用actions)。声明mutations的方式如:
const store = Vuex.Store({
// ...
mutations: {
increment(state) {
state.count++;
}
}
// ...
});
当我们需要改变一个状态的时候,就可以使用
this.$store.commit('increment')
来提交变更。有时候,我们还希望能够携带一些参数,携带的这些参数称之为载荷(payload),使用如:
1)第一种方式如:
this.$store.commit('increment', 1, 2, 3);
这种情况下,mutations
里的increment
方法能够以以下方式接收到参数:
increment(state, a, b, c) {
// code
}
其中a, b, c的值在这里将分别取到1, 2, 3。大多数情况下,推荐载荷为一个对象,因为这样子可以增加可读性:
// ...
increment(state, payload) {
console.log(payload.amount);
// code
}
// ...
store.commit('increment', {
amount: 10
})
还可以使用一个带有type
属性的对象来提交mutations,如:
store.commit({
type: 'increment',
amount: 123
});
这种情况下,整个对象都会成为payload
参数的值
当在对象中需要使用多个mutations的时候,vuex中也有mapMutations
辅助函数,用法如:
// in some component
methods: {
// 方式一(同名情况)
...mapMutations([
'increment',
'decrement'
]),
// 方式二(异名情况)
...mapMutations({
inc: 'increment'
})
}
// ...
vuex中推荐使用常量的形式来作为mutations事件名称,那么,我们就可以这么写:
// mutations-types.js
export const INCREMENT = 'INCREMENT';
// store.js
import Vuex from 'vuex';
import { INCREMENT } from './mutations-types';
const store = Vuex.Store({
// ...
mutations: {
[INCREMENT](state) {
// code
}
}
});
这种情况对于多人协作的大型项目是很有好处的,因为我们可以将事件名称抽离出来放在一个文件里,这样子整个项目的事件名称将一目了然。
规约
在使用vue+vuex进行开发的时候,我们需要遵守:
1)在store里事先声明好所需属性。这是因为Vue的属性是响应式的,事先声明好属性,可以让Vue事先做好预处理工作
2)如果需要添加新的属性,那么可以使用Vue.set(obj, 'newProp', 123)
的形式,或者用新对象来替换老对象,如:state.obj = { ...state.obj, newProp: 123 }
5、Actions
由于Vuex规定mutation都是同步的,那么当我们需要异步操作的时候,就需要有另外一种机制。而vuex里面的这种机制便是actions
,actions
可以包含任意的异步操作,而它所提交的是mutation
,而非直接改变状态,如:
const store = Vuex.Store({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++;
}
},
actions: {
increment(context) {
setTimeout(() => {
context.commit('increment')
}, 1000);
}
}
});
这里面,context
并非是store实例,而是当前模块的上下文(具体的内容在下面的模块
这一概念中将涉及)。mutation是用commit()
方法来触发,而action则是通过dispatch()
方法触发,调用方式有:
store.dispatch('increment');
store.dispatch('increment', 100);
store.dispatch({
type: 'increment',
amount: 100
})
而在组件中,当根组件里注入store
属性后,而可以通过this.$store.dispatch('xxx')
来分发action。和state
、getter
、mutation
一样,Vuex也提供了mapActions()
辅助函数,所以我们在组件内可以这么写:
// ...
methods: {
...mapActions([
'increment'
]),
...mapActions({
dec: 'decrement'
})
}
// ...
组合actions
可以结合Promise使用,如:
actions: {
actionsA({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('increment');
resolve();
}, 1000);
})
}
}
我们现在就可以这么使用了:
store.dispatch('actionsA').then(() => {
// code
});
或者在另一个action里进行组合,从而实现更为复杂的异步控制流程:
actions: {
// ...
actionsB({ dispatch, commit }) {
return dispatch('actionsA').then(() => {
commit('someOtherMutation')
});
}
}
利用async/await
,还可以进一步改写为:
actions: {
async actionsA({ commit }) {
commit('gotData', await getData())
},
async actionsB({ dispatch, commit }) {
await dispatch('actionsA'); // 等待actionsA完成
commit('gotOtherData', await getOtherData());
}
}
注意:一个
store.dispatch
在不同的模块中可以触发多个action函数,只有当触发完了所有的action函数后,返回的Promise才会执行
6、Modules
由于使用单一状态树,应有的所有状态都会集中到store
对象里,应用越复杂的时候,store对象就有可能越臃肿。这种情况下,vuex还提供了模块机制,允许对store进行分割。分割后的每个模块,都拥有自己的state、mutation、action、getter,而一个模块里还可以进一步分割出嵌套子模块。
一个简单的分割例子:
const moduleA = {
state: { /* code */ },
getters: { /* code */ },
mutations: { /* code */ },
actions: { /* code */ }
}
const moduleB = {
state: { /* code */ },
mutations: { /* code */ },
actions: { /* code */ }
}
const store = Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
});
如此一来,store就被分割成了子模块a和b,我们可以使用以下方式分别访问到子模块里的状态:
store.state.a;
store.state.b;
模块的局部状态
在一个模块内的getter、mutation,接收到的参数都是局部的状态,如:
const moduleA = {
state: {
count: 0
},
getters: {
doubleCount(state) {
// 这里的state是局部的state
}
},
mutations: {
increment(state) {
// 这里的state也是局部的state
}
}
}
而上文提到,actions里的context
并非store实例。实际上,它是当前模块的上下文,context
对象里暴露出了state
这一局部状态信息,而根实例上的state
,则通过rootState
来暴露,我们可以通过解构来获取这一些信息,如:
actions: {
someAction({state, commit, rootState}) {
// code
}
}
对于getter而言,rootState
则会作为第三个参数传递,如:
getters: {
someGetter(state, getters, rootState) {
// code
}
}
命名空间
在默认的情况下,getter、mutation、action是注册在全局命名空间
的,这样子的好处就是多个模块能够对同一mutation或者action做出响应。
但是我们可以启用namespaced
参数,来使得我们的模块变得更加的自包含和具有更高的重用性。启用namespaced
参数后,模块的所有getter、mutation、action都会根据注册的路径调整命名。看示例便知区别:
const moduleA = {
namespaced: true,
state: { /* code */ }, // state不受影响
getters: {
someGetter(){} // getters['moduleA/someGetter']
},
actions: {
someAction(){} // dispatch('moduleA/someAction')
},
mutations: {
someMutation(){} // commit('moduleA/someMutation')
},
modules: {
childA: {
getters: {
childGetter()
// 由于没有开启命名空间,所以继承父级命名空间getters['moduleA/childGetter']
}
},
childB: {
namespaced: true,
getters: {
childGetter()
// getters['moduleA/childB/childGetter']
}
}
}
}
虽然加了namespaced
参数后,模块的命名调整了。但是在模块内部,我们仍然可以不需要加这些路径,模块内部并不需要根据namespaced
参数启用还是关闭而做出调整。
那么,现在问题来了,如何在模块内访问全局的内容呢?
1)对于全局的state和getter,对于getter、mutation而言,可以使用第三个参数和第四个参数(分别是rootState
和rootGetter
),对于action而言,则context对象里也包含有rootState
和rootGetter
2)如果需要在全局命名空间内分发 action 或提交 mutation,那么将 { root: true }
作为第三参数传给 dispatch 或 commit 即可:
dispatch('someAction', null, { root: true });
commit('someMutation', null, { root: true });
带命名空间的辅助函数
如果我们启用了命名空间,那么在使用辅助函数的时候,我们通常需要这么写:
computed: {
...mapState({
a: state => state.some.nested.module.a,
b: state => state.some.nested.module.b
})
},
methods: {
...mapActions([
'some/nested/module/foo',
'some/nested/module/bar'
])
}
为了解决这个问题,我们可以给辅助函数传递第一个参数来指定命名空间路径,如:
computed: {
...mapState('some/nested/module', {
a: state => state.a,
b: state => state.b
})
},
methods: {
...mapActions('some/nested/module', [
'foo',
'bar'
])
}
模块动态注册
我们通过在Vuex.Store()
里配置module
属性来注册模块(这称为静态模块),但是如果我们在后续需要添加模块,那么该怎么办呢?解决方式是使用vuex提供的动态模块注册功能,如:
store.registerModule('myModule', {
// 注册模块 myModule
});
store.registerModule(['nested', 'module'], {
// 注册嵌套模块 nested/module
})