从零开始实现一个vuex插件
一、回顾官方vuex插件的使用如果你想自己实现一个vuex插件,首先要了解vuex插件的基本使用。我们在使用vuex的时候,通常会定义一个store.js文件,主要做了以下几件事:①导入vuex模块;从'vuex'导入Vuex;②导入Vue并使用vuex,即所谓安装vuex插件importVuefrom'vue';Vue.use(Vuex);③创建一个Store对象,并对外暴露④配置Store对象,即在创建Store对象时传递一个options参数,主要包括state(vuex的状态数据)、getters(从vuex导出的状态数据)、mutations(同步改变状态数据的方法),actions(异步改变状态数据的方法)exportdefaultnewVuex.Store({state:{count:0},getters:{newCount(state){returnstate.count*10;}},mutations:{syncAdd(state,payload){//同步state.count+=payload;//将状态数据中的计数值添加到payload},syncMinus(state,payload){//同步state.count-=payload;//从状态数据中的计数值中减去payload}},actions:{asyncMinus({commit},payload){//异步setTimeout(()=>{commit("syncMinus",10);},1000);});}});当然最后一步是在main.js中引入store.js暴露的store对象,并将store对象配置到Vue项目的根实例中。至此,你就可以在项目中使用vuex了。所谓使用vuex就是通过页面this.$store来获取和修改store对象中的状态数据2.Vuex原理分析Vuex的本质是使用一个全局的Vue对象实例作为数据源,而这个全局Vue实例对象Store将被挂载到整个AppVue实例上。conststore=newVue({data(){return{msg:"vuex"}}});constapp=newVue({el:"#app",beforeCreate(){//存储全局Vuex实例对象Mounted在app实例上this.$store=store;},mounted(){setTimeout(()=>{//2秒后修改Vuex中的数据store.msg="vuexchanged";},2000);}});msg-{{this.$store.msg}}
我们的模板使用全局Vuex实例store中的msg数据,当App实例渲染时,会创建一个渲染Watcher,渲染Watcher会在创建虚拟DOM时访问store中的数据,然后触发store中msg的get方法收集依赖,同时将当前渲染的Watcher添加到msg对应的Dep对象中观察者列表中,当store中的数据msg发生变化时,对应Dep对象的notify方法会被触发通知观察者列表中的所有观察者,因为渲染Watcher已经添加到store对象中msg对应的Dep的观察中在用户列表中,渲染Watcher会触发更新,渲染Watcher更新会引起页面更新。3、开始实现自己的vuex插件①因为我们需要通过Vuex.Store创建一个store对象,所以vuex插件导出了一个对象,里面有一个Store类。在vuex插件中声明一个Store类,同时创建存储对象时,需要传递一个options参数配置对象,包括state、getters、mutations、actions等属性配置,如:classStore{constructor(options){}}exportdefault{//vuex插件导出一个对象Store}②由于vuex插件需要先安装才能使用,所以vuex插件导出的对象必须包含一个install方法,添加一个install方法,install方法会接收到Vue的构造函数。为了方便,可以把vue的构造函数保存在插件中,vuex本质上是一个全局的大对象,数据可以被所有的组件共享,所以我们需要把这个store对象注入到所有的组件中,这样任何一个组件都可以获取到store通过this.$store对象注入根组件,如:letVue=null;constinstall=(VueConstructor)=>{Vue=VueConstructor;//保存Vue构造函数Vue.mixin({//Vue组件从外到内一步步渲染beforeCreate(){//在创建组件之前,组件中的beforeCreate不会被覆盖if(this.$options&&this.$options.store){//根组件实例对象,即newVue({})在main.js中创建的实例this.$store=this.$options.store;}else{//非-根组件实例对象this.$store=this.$parent&&this.$parent.$store;}}});}exportdefault{//vuex插件导出一个对象Store,install}③接下来就是处理创建Store对象时传入的options配置对象。首先我们处理vuex的状态数据,我们通过options传入store中的状态数据是一个普通的对象。如果直接保存状态对象,当组件修改状态数据时,页面无法感知到Store中状态数据的变化,因为状态数据是一个普通对象,没有添加getters和setters,所以我们可以直接将状态数据传递给Vue的构造函数,这样状态数据就会被添加到Vue实例对象中,也就是说Store对象的状态数据也是一个Vue实例对象,当页面有数据时在Store中使用,数据将作为Watcher添加到观察者列表中。当Store中的数据发生变化时,会通知所有观察者进行更新,从而实现页面中的数据更新classStore{constructor(options){this._vm=newVue({//将传递的状态数据传递给Vue,并创建一个Vue实例对象,以便页面在使用状态数据时更新页面数据:{state:选项.state}});}getstate(){//这样页面就可以通过this.$store.state获取状态数据returnthis._vm.state;//从创建的Vue实例对象中获取state状态数据}}④接下来就是对传入的getter进行处理,因为在getter对象中,每个getter都是一个函数,在使用的时候,我们通过获取派生的新状态点击getter的名称,即执行getter对应的函数,所以我们需要在获取getter的时候进行拦截,然后执行getter函数获取返回值,作为新的状态值返回,所以需要通过defineProperty来定义它,比如:classStore{constructor(options){constgetters=options.getters||{};//保存配置的getter,如果没有getter,{}this.getters={};//将getter中的属性定义到this.getters中,并根据状态变化重新设置:()=>{returngetters[getterName](this.state);//通过getterName获取值时,执行getter函数,作为新的状态值返回}});});}}⑤下一步是处理突变。突变相对简单。我们只需要直接遍历突变并重新赋值即可。突变是通过commit方法使用的,所以我们需要添加一个新的commit方法,并传递突变名称和数据。执行的时候,根据突变名找到对应的突变并执行,如:classStore{constructor(options){constmutations=options.mutations||{};//保存配置的突变,如果没有,{}this.mutations={};Object.keys(mutations).forEach((mutationName)=>{//将传入的突变函数名添加到this.mutations对象中,//并赋值一个新函数接收payload参数,并在函数内部执行相应的突变this.mutations[mutationName]=(payload)=>{mutations[mutationName].call(this,this.state,payload);//将this绑定到Store对象,使this成为mutation函数中的Store对象}});}commit(type,payload){//根据类型从this.mutations对象中获取对应的变异函数,传入参数执行this.mutations[type](payload);}}⑥下一步是处理动作。Actions和mutation一样,但是是通过dispatch调用的,所以需要增加一个dispatch方法,比如:classStore{constructor(options){constactions=options.actions||{};//保存配置的操作,如果没有则为{}this.mutations={};Object.keys(actions).forEach((actionName)=>{//将通过将动作函数的名称添加到this.actions对象,//并分配一个新函数来接收payload参数,并执行函数内部对应的actionthis.actions[actionName]=(payload)=>{actions[actionName].call(this,this,payload);//将this绑定到Store对象上,这样this就是Store中的对象动作函数}});}dispatch(type,payload){//根据typeaction函数从this.actions对象中获取对应,传入参数执行this.actions[type](payload);此时还有一个bug,就是在actions中执行asyncMinus的时候,里面的commit只是一个函数,并没有直接调用的对象,那么commit()函数执行的时候,this就会变成window对象,但是由于Vue代码被babel打包后会自动加上"usestrict",所以this会变成undefined,导致commit中获取不到mutation,所以我们必须重新定义commit和dispatch方法,先获取原来的commit和dispatch方法,然后在调用时强制绑定到Store对象,如:classStore{constructor(options){const{commit,dispatch}=this;//取出commit和dispatch函数this.commit=(type,payload)=>{//重新定义commit函数并将this重新绑定到Stroe对象commit.call(this,type,payload);}this.dispatch=(type,payload)=>{//为dispatch重新定义函数,将this重新绑定到Stroe对象上dispatch.call(this,type,payload);}}}4.总结vuex插件的主要用途是导出一个对象。由于需要安装vuex插件,所以需要有一个install方法,同时为了方便使用,作为一个全局对象,需要在所有组件中注入Store对象。为了创建Store对象,需要对外提供一个Store类,它主要接收一个配置对象,里面包含state、getters、mutations、actions,然后分别进行处理。为了在状态发生变化后更新页面上的状态,需要将状态放入Vue实例对象中,即创建一个Vue实例对象,将状态作为Vue实例对象的数据;getters是通过defineProperty重新定义,获取值时进行拦截,执行getter函数;可以遍历mutations和actions赋值给一个新的函数,当函数执行时,可以执行特定的mutations和actions