Vuex:从入门到入土

274 阅读6分钟

什么是Vuex,它是干什么的

官方解释:他是一个专为Vue.js应用程序开发的状态管理模式,它采用集中式存储管理应用所有组件的状态,Vuex是响应式的。

状态管理是什么

简单的来说,就是把需要多个组件共享的变量全部存储在一个对象之中,然后将该对象放入顶层的Vue实例之中,供其它组件使用,当然我们也可以手动声明一个共享对象然后将它嵌入每一个组件中,实现方式:

const shareThing={   }
Vue.prototype.shareThing = shareThing //通过Vue原型对每一个Vue实例中的组件进行赋值,在每个组件内部只需要通过this.sharThing.  即可访问shareThing中的具体值,但该方式不支持响应式

一般来说,用户的登录信息、头像、商品信息等状态信息需要供多个组件共享使用,而这些状态就可以放入Vuex进行响应式管理。

小案例

我们实现一个计数器,同时实现两个组件共享一个counter属性

  • 思路一:通过父子组件传值实现:
//子组件
<template>
    <h3>{{counter}}</h3>
</template>

<script>

export default ({
    name:'vuex',
    props:{
        counter:Number
    }
})
</script>

//父组件
<template>
  <div id="app">
    <h3>{{counter}}</h3>
    <button @click="counter++">+</button>
    <button @click="counter--">-</button>
    <vuex :counter='counter'/>
  </div>
</template>

<script>
import vuex from './components/vuex.vue'

export default {
  name: 'App',
  data() {
    return {
      counter:0
    }
  },
  components: {
  vuex
  }
}
</script>
  • 思路二:通过vuex集中管理:
//vuex配置文件
// 导入
import Vue from "vue";
import Vuex from 'vuex'

// 安装插件
Vue.use(Vuex)

// 创建对象
const store = new Vuex.Store({
    // 保存状态
    state:{
        counter:100
    },
    mutations:{

    },
    actions:{

    },
    getters:{

    },
    modules:{

    }
})
// 导出store
export default store

//在main.js中引用
import Vue from 'vue'
import App from './App.vue'
import store from './store'

Vue.config.productionTip = false


new Vue({
  render: h => h(App),
  store,
}).$mount('#app')
//类似于router,在这里实现了对每个组件注册$store属性,$store = store

//在组件中应用
<template>
  <div id="app">
    <h3>{{$store.state.counter}}</h3>
    <button @click="$store.state.counter++">+</button>
    <button @click="$store.state.counter--">-</button>
    <vuex/>
  </div>
</template>

action部分处理异步操作,mutations中处理同步操作,Devtools可以追踪哪个组件对state进行了修改

vue官方不推荐直接修改state,而是通过action和mutation对state的具体值进行修改。

vue官方提供了一个插件devtools,用于追踪Vuex中状态的变化,需要在浏览器中自行添加扩展插件。

回到之前的情景,我们不直接调用$store.state.counter来对状态中的counter的值进行更改,而是通过在配置文件中注册mutations方法,来对state中的counter进行修改,然后在每个组件中调用mutations中声明的方法。mutations中的方法都是同步方法,如果是异步操作的话,devtools将无法追踪到。

//配置文件   
 mutations:{
        incerement(state){
            state.counter++
        },
        decerement(state){
            state.counter--
        }
    }
//组件调用方法
 methods:{
    add(){
      this.$store.commit('incerement')
    },
    sub(){
      this.$store.commit('decerement')
    }
  }

提交mutation是store(state)更新的唯一方式,通过commit(”)来使用mutation中的回调函数

当我们在mutation中处理数据时需要用到外部传入的数据时(例如上例中我们增加一个按钮,让其实现每按一次counter就是增加5),此时需要我们在使用commit调用回调函数时传入额外参数(payload),实现上述要求

//组件中 
<button @click="addNum(5)">+5</button>
 methods:{
    addNum(number){
      this.$store.commit('incerementNum',number)
    }
//store文件中
    mutations:{
        incerementNum(state,number){
            state.counter += number
        }
    },

当然除了直接使用commit进行提交,官方还提供了另外一种提交封装,一个包含type属性的对象,我们以上述例子为例

//组件中    
addNum(number){
      // 1.简单的提交封装
      // this.$store.commit('incerementNum',number)
      // 2.另一种提交封装
      this.$store.commit({
        type:'incerementNum',
        payload
        // payload是一个对象,内部属性为type:'incerementNum'和number:
      })
    }
//store文件中
    mutations:{
        incerementNum(state,payload){
            state.counter += payload.number
        },
    }

Mutation中的处理方式是将整个commit的对象作为payload进行使用。

当然,我们还可以对Mutation中的函数进行一定处理,以便于导入调用

Mutation的响应式原理

从先前的state学习中得知,state是响应式的(当state中的数据发生改变时,Vue组件将会自动更新)也就是说,我们申明state中的对象以及其变量时,这些属性都将加入响应式系统中进行实时监听,当属性发生变化时,将会通知界面中所有用到该属性的地方,让界面进行刷新,故我们需要在store中初始化好需要的属性。如果需要改变现有的对象中的属性,可以直接在mutation中声明函数,完成该功能,然后再在组件中使用commit调用即可。当我们需要给state中的对象添加新属性时,方式有:

  • 使用Vue.set(obj,’newProp’,value)

值得注意的是,state中的属性如果要加入响应式系统的话,必须提前初始化,或使用上方方法增加属性。若单纯使用js已有的在对象中添加属性的方法(state.xxxx[‘xxx’]=’xx’),则添加的属性xx不会加入响应式系统,不会被实时监听,但如果检查属性的话会发现能够实现属性的增加,但由于界面不会随之刷新,故使用该方式添加属性没有实质作用。 相应的,正确的添加方法为:Vue.set(xxxx,’xxx’,’xx’)

同样的,如果我们想要删除某个属性,直接使用js中已有的方法delete(delete state.xxxx.xxx),也无法实现在响应式系统中的删除。相应的,Vue.delete(state.xxxx,’xxx’)可以实现

单一状态树

单一状态树(single source of truth)指的是将所有的状态(state)全部保存在一个系统中,即将所有的需要进行管理的state全部保存在一个store对象里,而不是分别存于store1、store2等。如此操作有助于今后的管理与维护。

采用单一状态树,可以让我们以最直接的方式找到某个状态的片段

getters

getters,类似于computed属性,用于计算state中的值,一般用于呈现state中的值经过处理后的值,操作方式与computed基本类似。

值得一提的是,当需要使用已经在getters中定义过的值时,只需要在后续传参时传入getters,然后通过调用getters.来实现对其中值的访问(第二个传入的参数无论名称都默认为是传入getters)

getters:{
       counterPower(state){
       return state.count* state.count
       },
       fitPeople(state){
       return state.People.filter(s => s.age>=20)
       },
       fitPeopleLength(state, getters){
       return getters.fitPeople.length
       }
}

回到上例,当我们想从外部传入数据而不是在内部写死筛选年龄不小于20的人时,我们需要对getters内部传入的数值进行更改。

fitPeople(state){
//返回一个函数后,便可以传入参数,调用该函数
         return function(age){
             return state.People.filter(s => s.age>=age)
         }
}

使用箭头函数对上方函数进行进一步简化得到

fitPeople(state){
//返回一个函数后,便可以传入参数,调用该函数
         return age => {
             return state.People.filter(s => s.age>=age)
         }
}

actions

mutations中的方法全是同步操作,如果出现异步操作的话,devtools将无法实现跟踪,很难去debug,此时需要使用action(异步操作)。

值得注意的是,在action中并不是直接对state中进行更改,而是通过commit使用mutation中的函数对state进行修改(这样操作才能让devtools实现正常追踪)

Vuex工作流程

action的使用方式与mutation差不多,使用this.$store.dispatch调用在action中已声明好的函数。值得注意的是,在action中不能直接对state中的属性或对象进行更改,而是调用mutation中的方法进行更改,具体案例如下:

 //配置文件 
 mutations:{
        updateState(state){
            state.People[1].name = 'sunShineBoy'
        }
    },
    actions:{
        // context上下文
        aUpdateState(context, payload){
            setTimeout(() => {
                context.commit('updateState');
                console.log(payload);
            }, 1000);

        }
    }
//组件
  methods:{

    aUpdateState(){
      this.$store.dispatch('aUpdateState','我是sunshine boy')
    }
  }

当然,如果我们需要在异步处理完成后提醒用户已经完成异步处理的话,我们可以在dispatch传入数据时进行更改(传入一个大的对象):

//组件
aUpdateState(){
      this.$store.dispatch('aUpdateState',{
        msg:'我是sunshine boy',
        success:()=>{
          console.log('我终于成为阳光男孩辣');
        }
      })
    }
//配置文件
aUpdateState(context, payload){
            setTimeout(() => {
                context.commit('updateState');
                console.log(payload.msg);
                payload.success();
            }, 1000);
        }

当然上述操作还可以通过Promise进行操作。

//配置文件        
aUpdateState(context, payload){
            return new Promise((resovle,reject)=>{
               setTimeout(() => {
                context.commit('updateState');
                resovle('传入的数据')
            }, 1000); 
            })    
        }
//组件
 aUpdateState(){
      this.$store.dispatch('aUpdateState','我是sunshine boy')
      .then(res=>{
        console.log('我是阳光大男孩辣!');
        console.log(res);
      })
    }

action这个方法本身就可以返回一个Promise,在返回这个promise之后就可以在其他地方(例如dispatch中)拿到这个promise,然后在后端就可以就这加上.then等

modules

模块化

modules的核心就是模块化,将复杂的应用进行抽离,抽离出若干个小模块,并分别进行管理,如上图,若要访问定义的moduleA的状态,则需要通过this.store.state.a来进行访问(这里的a是在modules中注册的名字),如果要使用声明的modules中的mutations还是直接在组件中使用commit,需要注意的是,各个模块间声明的mutations中的方法名不要重合。外部调用commit后,会首先在store中的mutations中进行方法的寻找,未果,再在各个模块进行寻找。gettersmutations相同,也是直接在外部调用store.state.a来进行访问(这里的a是在modules中注册的名字),如果要使用声明的modules中的mutation,s还是直接在组件中使用commit,需要注意的是,**各个模块间声明的mutations中的方法名不要重合**。外部调用commit后,会首先在store中的mutations中进行方法的寻找,未果,再在各个模块进行寻找。getters和mutations相同,也是直接在外部调用store.getters即可。

如果想要在module中使用根(即store)的一些状态的话,只需要在传输参数的时候传入rootState.需要注意的是,模块在声明actions中的异步操作函数时使用commit,只能调用该模块中mutations中已声明的函数方法。在actions中的方法,我们会传入参数context(上下文),我们打印一下这个参数。

不难看出context中不仅包含该模块的getters和state还包含根中的getters和state,故需要访问根中的getters和state只需要通过context.rootGetters或者context.rootState即可进行访问,当然在这里也可以对对象context进行解构,传入context中所包含的并且我们所需要的参数。

  • ps.只有在模块里面才会有根的概念