首先,Vuex是什么,官网介绍说Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。我的理解就是Vuex就是类似于sessionStorage这样管理数据(本地存和取)的一种技术方案。
既然vuex类似于sessionStorage,那为何我们还要学习vuex,直接用sessionStorage和localStorage不就好了?这个问得好,我来描述一种场景:多个视图(view)组件都要用到某一条数据(状态),当这条数据发生变化的时候,依赖于该数据(状态)的相关视图(view)都要跟着即时更新。这种场景在工作中非常常见,我说一个自己碰到的例子,以前有一个react项目,其中有个功能是在pc页面自定义小程序页面,然后整个PC页面有三个组件组成,在三个组件中还有其他的很多子组件。然后一开始的做法就是通过事件和组件间传值来进行整个页面数据同步更新,后面随着组件越来越多,功能越来越复杂,麻烦和问题也就越来越多。然后每一个后面来接手的同事看代码都要看好一阵,长痛不如短痛...
对的,在工作中这种常见的多个组件依赖于同一条数据(状态),需要即时响应更新的情况,vuex的价值就体现出来了。这种情况下,vuex相比其他实现手段,就要简单干脆方便多了!先看一个小例子,看看vuex和localStorage、sessionStorage的区别,上图:
如图,vuexPageA页面中引用了三个组件,每个组件都分别从localStorage、sessionStorage、vuex中取了一个值。点击按钮加1的时候,vuex的值是及时更新了,其他需要刷新才能更新。总结一下:
相关代码见:https://github.com/xiaotanit/tan_vue/blob/master/src/views/vuex/VuexPageA.vue
每一个Vuex应用的核心就是store(仓库),“store"基本上就是一个容器。Vuex使用单一状态树,相当于用一个对象(store)就包含了全部的应用层级状态,也就是说每个应用也只包含一个store实例。因此Vuex的使用从new一个Vuex.Store实例(store实例)开始。store实例中的State属性就是用来存放Vue应用的所有的状态。先来看要给最简单的包含State属性的store实例:
import Vuex from 'vuex'
import Vue from 'vue'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
count: 0,
},
})
后面的mutations、getters、actions再慢慢往里面加入代码。
store实例创建,如何应用?Vue实例创建时,提供了一个store选项,可以让Vuex通过store选项,将store实例对象从根组件”注入“到每一个子组件中:
import Vue from 'vue'
import App from './App.vue'
import router from './router.js'
//vuex 之 store实例对象
import store from './api/store/index'
new Vue({
router,
store, // 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件
render: h => h(App)
}).$mount('#app')
store实例注入根组件后,应用中的每个组件中通过this.$store指的就是该store实例对象。那么现在如何在Vue组件中展示store中的state状态(数据)呢?由于Vuex的状态存储是即时响应的,从store实例中读取状态最简单的方法就是在Vue组件中”计算属性“computed中返回某个状态。每当store.state中某个状态变化的时候,都会重新求取计算属性,并且触发更新相关联的DOM。
mapState是一个辅助函数,当我们应用中一个组件需要获取store中多个状态的时候,使用mapState辅助函数可以帮助我们更加方便生成计算属性。看看下面的应用测试代码:
import { mapState } from 'vuex';
export default {
data(){
return {
localCount: 88
}
},
mounted(){
console.log("...store对象:", this.$store);
},
computed:{
localStorage_count(){
return localStorage.getItem('localStorage_count')
},
//使用对象展开符"...",可以将对象目标对象混入到外部对象中
...mapState({
sessionStorage_count(){
return sessionStorage.getItem('sessionStorage_count')
},
vuex_count: state => state.count, //箭头函数可以使代码更简练
vuex_count_alias: 'count', //传字符串参数'count'等同于 state => state.count
// 为了能够使用 `this` 获取局部状态,必须使用常规函数
countPlusLocalState (state) {
return state.count + this.localCount
}
}),
},
}
有时我们需要从store中的state种派生出一些状态,比如对store中的某一个状态(数据)进行筛选过滤,然后特别是当有多个组件需要用到这种状态(数据)时,“getter"就出场了!Vuex允许我们在store中定义”getter"(可以认为是store对象的计算属性)。就像计算属性一样,getter的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。Getter接受state作为其第一个参数:
export default new Vuex.Store({
state: {
count: 0,
todos: [
{ id: 1, text: '金戈铁马,气吞万里如虎', done: true },
{ id: 2, text: '老骥伏枥,志在千里', done: false },
{ id: 3, text: '周公吐哺,天下归心', done: true },
{ id: 4, text: '但使龙城飞将在,不教胡马度阴山', done: false },
]
},
//Vuex允许我们再store中定义"getter"(可以认为是store的计算属性)。
// 就像计算属性一样,getter的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
getters: {
doneTodos: state => {
console.log('...state.getters.donwTodos...')
return state.todos.filter(todo => todo.done)
},
//Getter也可以接受其他getter作为第二个参数
//getter在通过属性访问时是作为Vue的响应式系统的一部分缓存其中的
doneTodosCount: (state, getters) => {
console.log('...state.getters.doneTodosLength...', getters.doneTodos)
return getters.doneTodos.length;
},
//通过方法访问:通过让getter返回一个函数,来实现给getter传参。
//getter在通过方法访问时,每次都会去进行调用,而不会缓存结果。
getTodoById: (state) => (id) => {
console.log('...state.getters.getTodoById...: ', id);
return state.todos.find(todo => todo.id === id);
}
},
})
Getter应用:Getter会暴露为 store.getters 对象,然后在组件中,我们可以通过this.$store.getters来得到getter。getter里面的属性,可以返回属性,也可以返回方法。如果getter通过属性访问时是作为Vue的响应式系统的一部分缓存,首次调用后再次调用时就会调用缓存,只有该属性的依赖值变化时,再次调用该属性才会重新调用重新缓存。如果getter通过方法访问时,每次都会去进行调用,而不会缓存结果。组件中应用测试代码:
methods:{
//state.getters调用
stateGettersProperty(){ //getters属性调用, 属性调用会被缓存
console.log(this.$store.getters.doneTodos);
console.log(this.$store.getters.doneTodosCount);
},
stateGettersMethod(){ //方法调用,每次都会去进行调用,而不会缓存结果。
console.log(this.$store.getters.getTodoById(2).text);
console.log(this.$store.getters.getTodoById(3).text);
},
addTodo(){ //增加数据
let count = this.$store.state.todos.length;
let obj = {
id: count + 1,
text: (count+1) + '***' + (count+1),
done: count % 2
}
this.$store.commit('addTodos', obj);
},
}
mapGetters也是一个辅助函数,可以将store对象中的getter映射到局部计算属性:
import { mapGetters } from 'vuex'
export default {
// ...
computed: {
// 使用对象展开运算符将 getter 混入 computed 对象中
...mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
])
}
}
如果你想将一个 getter 属性另取一个名字,使用对象形式:
mapGetters({
// 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
doneCount: 'doneTodosCount'
})
上面说的mapState、getters、mapGetters都是对store对象中的状态(state)进行应用,如果想更改Vuex的store对象中的状态(state),必须要用mutation。Vuex中的mutation非常类似于事件:每个mutation都有一个字符串的事件类型(type)和一个回调函数(handler) 。这个回调函数就是我们实际进行状态更改的地方,并且它会接受state作为第一个参数:
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state) {
// 变更状态
state.count++
}
}
})
mutation里面handler调用通过store.commit来调用,调用方式有“载荷(payload)"和“对象风格”两种方式:
// ...
mutations: {
increment (state, n) {
state.count += n
}
}
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
Action类似于mutation,但是Action提交的是mutation,不能直接变更状态;另外Action可以包含任意异步操作。在组件中使用this.$store.dispatch('***')调用action,或者使用mapActions辅助函数将组件中的methods映射为store.dispatch调用。
State、Getter、Mutation、Action的一些应用测试代码见:https://github.com/xiaotanit/tan_vue/blob/master/src/views/vuex/VuexPageB.vue
由于使用单一状态树,应用的所有状态(数据)会集中到一个比较大的对象。当应用变得非常复杂时,store对象就有可能变得相当臃肿。为了解决这种问题,Vuex允许我们将store分隔成模块(module)。每个模块都有自己的state、mutation、action、getter、甚至是嵌套子模块。
默认情况下,模块内容的action、mutation和getter是注册在全局命名空间的,这样使得多个模块能够对同一mutation或action作出响应。因此为了让模块具有更高的封装度和复用性,我们可以在每个子模块中添加namespaced: true属性,这样表示该模块成为了带命名空间的模块。这样后面再调用该模块的getter、action和mutation时需要带上该模块名称+调用的属性或方法。下面写一个示例代码:
新建三个js文件moduleA.js、moduleB.js、moduleStore.js,其中moduleA和moduleB分别为子模块。
moduleA.js:
const state = {
countA: 99
}
const mutations = {
increment(state){
state.countA++
},
decrement(state){
state.countA--
}
}
const getters = {
doubleCount(state){
return state.countA * 2
}
}
const actions = {
add({ commit }){
setTimeout(function(){
commit('increment')
}, 50)
},
minus({ commit }){
setTimeout(()=>{
commit('decrement')
}, 500)
}
}
export default {
namespaced: true, //表示设置命名空间
state,
mutations,
getters,
actions
}
moduleB.js:
const state = {
countB: 11
}
const mutations = {
increment(state){
state.countB++;
},
decrement(state){
state.countB--;
}
}
//getters类似state里面属性的计算属性
const getters = {
doubleCount(state){
return state.countB * 2;
}
}
const actions = {
add({ commit }){
commit('increment')
},
minus({ commit }){
commit('decrement')
}
}
export default {
namespaced: true,
state,
getters,
mutations,
actions
}
moduleStore.js:
/*
* 当项目大了后,为了责任清晰,目标明确,更易管理,将store拆成多个module形式
* */
import moduleCountA from './moduleA'
import moduleCountB from './moduleB'
import vuex from 'vuex'
import vue from 'vue'
vue.use(vuex)
export default new vuex.Store({
modules: {
moduleCountA,
moduleCountB
}
})
再新建一个VuexPageC.vue页面,测试调用,js代码如下:
import { mapGetters, mapActions, mapMutations } from 'vuex'
export default {
computed:{
...mapGetters({
doubleCountA: 'moduleCountA/doubleCount',
doubleConunB: 'moduleCountB/doubleCount'
})
},
methods: {
...mapActions({
//moduleA模块的actions
addCountA: 'moduleCountA/add',
minusCountA: 'moduleCountA/minus',
//moduleB模块的actions
addCountB: 'moduleCountB/add',
minusCountB: 'moduleCountB/minus'
}),
...mapMutations({
//moduleA模块的mutions
incrementA: 'moduleCountA/increment',
decrementA: 'moduleCountA/decrement',
//moduleB模块的mutions
incrementB: 'moduleCountB/increment',
decrementB: 'moduleCountB/decrement'
}),
}
}
页面效果如图:
完整VuexPageC.vue页面代码见:https://github.com/xiaotanit/tan_vue/blob/master/src/views/vuex/VuexPageC.vue