2

一、回顾一下官方vuex插件的使用

要想自己实现一个vuex插件,就必须先了解一下vuex插件的基本使用,我们在使用vuex的时候,通常会定义一个store.js文件,里面主要就是干了以下几件事:

① 引入vuex模块

import Vuex from 'vuex';

② 引入Vue并使用vuex,即所谓的安装vuex插件

import Vue from 'vue';
Vue.use(Vuex);

③ 创建Store对象并对外暴露
④ 配置Store对象,即在创建Store对象的时候传递一个options参数,里面主要包括state(vuex的状态数据)、getters(vuex中派生的状态数据)、mutations(同步改变状态数据的方法)、actions(异步改变状态数据的方法)

export default new Vuex.Store({
    state: {
        count: 0
    },
    getters: {
        newCount(state) {
            return state.count * 10;
        }
    },
    mutations: {
        syncAdd(state, payload) { // 同步
            state.count += payload; // 将状态数据中的count值加payload
        },
        syncMinus(state, payload) { // 同步
            state.count -= payload; // 将状态数据中的count值减payload
        }
    },
    actions: {
        asyncMinus({commit}, payload) { // 异步
            setTimeout(() => {
                commit("syncMinus", 10);
            }, 1000);
        });
    }
});
当然,还有最后一步就是在main.js中引入store.js对外暴露的store对象,并且将该store对象配置到Vue项目的根实例上,至此,就可以在项目中使用vuex了,所谓使用vuex,就是在页面中通过this.$store获取和修改store对象中的状态数据。

二、Vuex原理解析

Vuex本质是用一个全局的Vue对象实例作为数据源,而这个全局的Vue实例对象Store又会被挂载到整个App Vue实例上
const store = new Vue({
   data() {
     return {
        msg: "vuex"
     }
   }
});

const app = new Vue({
  el: "#app",
  beforeCreate () {
    // 将全局的Vuex实例对象store挂载到app实例上
    this.$store = store;
  },
  mounted () {
    setTimeout(() => {
      // 2秒后修改Vuex中的数据
      store.msg = "vuex changed";
    }, 2000);
  }
});
<div id="app">
   msg - {{this.$store.msg}}
</div>
我们的模板中使用到了全局的Vuex实例store中的msg数据,当App实例渲染的时候,会创建一个渲染Watcher渲染Watcher在创建虚拟DOM的时候就会访问到store中的数据,进而触发store中msg的get方法收集依赖,同时会将当前渲染Watcher添加到msg对应的Dep对象中的观察者列表中,当store中的数据msg发生改变后,就会触发对应Dep对象的notify方法通知其观察者列表中的所有观察者,由于渲染Watcher已经添加到了store对象中msg对应的Dep的观察者列表中,所以渲染Watcher就会触发更新渲染Watcher更新就会导致页面更新

三、开始实现自己的vuex插件

因为我们需要通过Vuex.Store来创建store对象,所以,vuex插件导出的是一个对象,并且其中有一个Store类,在vuex插件中声明一个Store类,同时在创建Store对象的时候需要传递一个options参数配置对象,有stategettersmutationsactions等属性配置,如:

class Store { 
    constructor(options) {

    }
}

export default { // vuex插件导出的是一个对象
    Store
}

由于vuex插件需要安装才能使用,所以vuex插件导出的对象中必须包含一个install方法,添加一个install方法,install方法会接收Vue构造函数,为了方便使用,可以在插件中将Vue的构造函数进行保存,vuex本质是一个全局的大对象,数据可以被所有组件共享,所以我们需要将这个store对象注入到所有组件内,以便任何一个组件都可以通过this.$store获取到注入到根组件内的store对象,如:

let Vue = null;
const install = (VueConstructor) => {
    Vue = VueConstructor; // 将Vue构造函数保存起来
    Vue.mixin({ // Vue组件是从外到内逐级渲染的过程
        beforeCreate () { // 在组件创建之前,不会覆盖组件中的beforeCreate
            if(this.$options && this.$options.store) { // 根组件实例对象,即main.js中new Vue({})创建的实例
                this.$store = this.$options.store;
            } else { // 非根组件实例对象
                this.$store = this.$parent && this.$parent.$store;
            }
        }
    });

}
export default { // vuex插件导出的是一个对象
    Store,
    install
}

接下来就是要处理创建Store对象的时候传递进来的options配置对象了,首先我们先对vuex的state状态数据进行处理,我们通过options传递进来的state状态数据是一个普通对象,如果直接将这个state对象保存起来,那么当组件修改状态数据的时候,页面中就无法感知到Store中状态数据发生了变化,因为状态数据是一个普通对象,没有添加getter和setter,所以我们可以直接把状态数据传递给Vue的构造函数,这样状态数据就会被添加到这个Vue实例对象上了,也就是说Store对象的状态数据也是一个Vue实例对象,当页面中有使用到Store中的数据,那么该数据就会被当做Watcher添加到观察者列表中,当Store中数据发生变化,就会通知所有观察者进行更新,从而实现页面中数据更新。

class Store {
    constructor(options) {
        this._vm = new Vue({ // 将传递的state状态数据传递给Vue并创建Vue实例对象,以便页面中使用到state数据的时候能够更新页面
            data: {
                state: options.state
            }
        });
    }
    get state() { // 以便页面中可以通过this.$store.state获取到状态数据
        return this._vm.state; // 从创建的Vue实例对象中获取state状态数据
    }
}

接下来就是要处理传递进来的getters,因为getters对象中,每个getter都是一个函数,而使用的时候,我们通过点getter的名称获取的到派生出来的新的状态,即执行getter对应函数,所以我们需要在获取getter的时候进行拦截,然后执行getter函数获取到返回值之后再返回作为新的状态值,所以我们需要通过defineProperty进行定义,如:

class Store {
    constructor(options) {
        const getters = options.getters || {}; // 保存配置的getters,如果没有则为{}
        this.getters = {};
        // 把getters中的属性定义到this.getters中,并且根据状态变化,重新执行此函数
        Object.keys(getters).forEach((getterName) => {
            Object.defineProperty(this.getters, getterName, {
                get: () => {
                    return getters[getterName](this.state); // 通过getterName获取值的时候,执行getter函数并返回作为新的状态值
                }
            });
        });
    }
}

接下来就是要处理mutations了,mutations相对较简单,我们只需要直接遍历mutations重新赋值即可,mutations使用的时候是通过commit方法,所以需要新增一个commit方法,并传递mutation的名称和数据,执行的时候,根据mutation名称找到对应的mutaion并执行,如:

class Store {
    constructor(options) {
        const mutations = options.mutations || {}; // 保存配置的mutations,如果没有则为{}
        this.mutations = {};
        Object.keys(mutations).forEach((mutationName) => {
            // 将传递过来的mutation函数名添加到this.mutations对象中,
            // 并赋值一个新的函数接收payload参数,函数内部执行对应的mutation
            this.mutations[mutationName] = (payload) => {
                mutations[mutationName].call(this, this.state, payload); //绑定this为Store对象,以便在mutation函数中this是Store对象 
            }
        });
    }
    commit(type, payload) {
        // 从this.mutations对象中根据type取出对应的mutation函数并传入参数执行
        this.mutations[type](payload);
    }
}

接下来就是要处理actions了,actions同mutations一样,只不过是通过dispatch调用,所以需要新增一个dispatch方法,如:

class Store {
    constructor(options) {
        const actions = options.actions || {}; // 保存配置的actions,如果没有则为{}
        this.mutations = {};
        Object.keys(actions).forEach((actionName) => {
            // 将传递过来的actions函数名添加到this.actions对象中,
            // 并赋值一个新的函数接收payload参数,函数内部执行对应的action
            this.actions[actionName] = (payload) => {
                actions[actionName].call(this, this, payload); //绑定this为Store对象,以便在action函数中this是Store对象 
            }
        });
    }
    dispatch(type, payload) {
        // 从this.actions对象中根据type取出对应的action函数并传入参数执行
        this.actions[type](payload);
    }
}

此时还存在一个bug,那就是当执行actions里的asyncMinus时,里面的commit只是一个函数没有对象直接调用,那么commit()函数执行的时候,其中的this将会变成window对象,但是由于Vue代码经babel打包后会自动加上"use strict",所以this将会变成undefined,将导致commit中获取mutation失败,所以我们必须重新定义commit和dispath方法,首先获取到原来的commit和dispatch方法,再调用的时候强制绑定为Store对象即可,如:

class Store {
    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);
        }
    }
}

四、总结

vuex插件主要就是导出一个对象,由于vuex插件需要安装,所以需要有一个install方法,同时为了方便使用并作为一个全局对象,需要将Store对象注入到所有组件中,为了创建Store对象,需要对外提供一个Store类,其主要接收一个配置对象,里面包含stategettersmutationsactions,然后分别对其进行处理,为了能够让state发生变化后能够在页面上更新,所以需要将state放到Vue实例对象中,即创建一个Vue实例对象,将state作为Vue实例对象的data;getters则是通过defineProperty重新定义,在获取值的时候进行拦截,执行getter函数;mutations和actions则是遍历并赋值为一个新函数即可,函数执行的时候,执行具体的mutation和action即可。


JS_Even_JS
2.6k 声望3.7k 粉丝

前端工程师