vuex作为一个共享的数据对象,用于不同组件的共用一个数据,并且动态响应数据变化,配合computed属性,可以对数据进行缓存。

在vuex里面,在异步处理actions上,最终也还是要用到commit同步操作方法操作数据,并不是actions方法本身处理了数据,原因是因为需要有操作记录,方便在插件上查看。

废话不多说,直接上底层原理。

原理

vuex其实就是一个重新封装的new Vue 对象,他的动态响应数据就是data属性,而commit这些方法,都只是回调而已。

import Vue from "vue";

const Store = function(options = {}) {
    const {
        state = {}, mutations = {}
    } = options;
    this._vm = new Vue({
        data: {
            $$state: state
        },
    });
    this._mutations = mutations;
};

Store.prototype.commit = function(type, value) {
    if (this._mutations[type]) {
        this._mutations[type](this.state, value);
    }
};

Object.defineProperties(Store.prototype, {
    state: {
        get: function() {
            return this._vm._data.$$state
        }
    }
});


export default {
    Store
};

引入一个vue对象,然后创建一个Store的构造函数,并接收一个参数options,options设置一个默认空对象。

Store构造函数内部我们解构这个options,获取到state和mutations对象,如果没有,我们也默认是一个空对象。

给Store构造函数的this,也就是prototype增加一个_vm属性,该属性是一个new Vue对象,并且在这个vue的data属性上,我们使用$$state的key,其属性值指向了之前解构的state对象。

这里我们就将数据给监听了,利用vue的特性。

现在我们需要注册mutations对象,mutations本身就是同步操作state属性的方法的一个对象集合。

直接this._mutations = mutations,将解构的mutations挂载到Store的prototype上。

创建commit方法

commit方法用于统一调用mutations中的对应的方法,我们看个vuex的例子:

this.$store.commit("方法名",value);

所以,我们也要给Store构造函数添加一个commit方法,这个方法接收两个参数,一个是要调用的方法名,一个是值。

Store.prototype.commit = function(type, value) {
    if (this._mutations[type]) {
        this._mutations[type](this.state, value);
    }
};

然后我们if判断,如果this._mutations中存在这个方法,我们就调用和这个方法,并且把state和value值传过去。

注意

此时我们的this.state是没有的,你可以仔细往上看,在添加commit方法的时候,我们还没有对this这个对象,也就是Store本身添加一个state属性。

defineProperties配置一个state属性

defineProperties是defineProperty的加强版,defineProperty一次只能修改或者创建一个属性,defineProperties可以多个。

Object.defineProperties(Store.prototype, {
    state: {
        get: function() {
            return this._vm._data.$$state
        }
    }
});

defineProperties接收两个参数,第一个要定义的对象,第二个是定义的参数。

定义一个state属性,他的get方法返回this._vm._data下的$$state属性。

在vue对象里面,data对象是挂载的_data下的。

使用defineProperties定义的属性,是无法被for in遍历出key值得,并且我们配置set方法,所以这个state也无法被改写。

你无法Store.state = {}去改变state的指向,或者设置他的值,都不行。

然后我们导出这个State对象。

因为vuex的使用一般是这样:

new Vuex.Store()

所以我们也导出一个对象,对象里面包含Stote

这样我们在使用时:

import miniVuex from "./mini-vuex";
const miniStore = new miniVuex.Store({});
Vue.prototype.$miniStore = miniStore;

基本保持一致。

例子

import miniVuex from "./mini-vuex";
const miniStore = new miniVuex.Store({
    state: {
        test: "我是一个测试的值"
    },
    mutations: {
        setTest(state, value) {
            state.test = value;
        }
    }
});

Vue.prototype.$miniStore = miniStore;

这样我们可以通过this.$miniStore来使用。

配置getters

首先我们要知道,getters一般是怎么用的。

const miniStore = new miniVuex.Store({
    state: {
        test: "我是一个测试的值"
    },
    getters:{
      test(state,getters){
        return state.test;
      }
    },
    mutations: {
        setTest(state, value) {
            state.test = value;
        }
    }
});

getters是一个对象,对象里面的方法,接收两个参数,一个是state,一个是getters,state就不用多说了,而getters获取的是getters对象本身,用户获取其他值。

而我们之前说过,getters实际上会使用conputed缓存,所以我们需要在miniVuex中,解构到这个getters,然后for循环遍历出里面的内容,并绑定到computed上。

const Store = function Store(options = {}) {
  const {state = {}, mutations={}, getters={}} = options;
  
  const computed = {}
  
  const store = this;
  store.getters = {};
  
  for (let [key, fn] of Object.entries(getters)) {
    computed[key] = function () { 
      return fn(store.state, store.getters); 
    };
  };

  this._vm = new Vue({
    data: {
      $$state: state
    },
    computed,
  })
  this._mutations = mutations
};

这里有几个关键的点:

Object.entries是一个对象的方法,他会将对象里面的键值对转换为数组,例:

const obj = {a:1,b:2};
console.log(Object.entries(obj));
//输出:[[a,1],[b,2]]

然后我们for of循环这个数组,该遍历方法是es6的方法,他会创建一个用于每次遍历的对象,如果是数组的话,第一个对象就是对应数组下标0.

然后使用解构,我们将key和fn解析出来,fn实际上就是创建vuex是,getters中我们预先写好的方法。

然后我们将它挂载到computed空对象上面,这里使用了一个工厂函数抛出,如果不使用工厂函数,这里我们首先要传入两个参数(state,getters),并且return出一个值,这就导致运行完毕后直接return,后面的函数就不执行了。所以我们需要一个函数包裹,这样return停止的是这个工厂函数,外面的代码依旧可以运行。

传入的两个参数

我们需要传入state和getters。

store的话可以通过this.store获取,但是在函数里面this的指向会发生变化,所以我们创建一个变量store来指向这个this

于是const store = this;,第一个参数为store.state

而getters暂时还没有,所以我们需要手动创建,于是:store.getters = {};,第二个参数store.getters

这个时候,我们已经将getters挂载到Store这个构造函数对象上面,而computed挂载到this._vm.computed上,而vue会将computed中的属性全都挂载到根对象上,所以我们可以直接通过this._vm.test获取到数据。

但是这个时候并没有达到我们的要求,我们的getter的调用方法是这样的:

this.$store.getters.test

目前的调用方式是:miniVuex._vm.test

所以我们还需要改写store.getters的get方法。

使用Object.defineProperty我们将对象的get方式改变一下

 for (let [key, fn] of Object.entries(getters)) {
    computed[key] = function () { 
      return fn(store.state, store.getters); 
    };

    //改写get
    Object.defineProperty(store.getters, key, {
      get: function () {
        return store._vm[key]; 
      },
    });
    
  };

在for循环里面,我们对store.getters对象,创建一个key,而这个key直接返回store._vm[key],store._vm=vue根对象,所以实际上这里我们return的是computed的值,缓存目的达到。

这个时候:

miniStore.getters.test

是有值得了。

这样我们的getters就写完了。

挂载到vue对象上

先上源码:

let Vue;

function install(_Vue) {
    Vue = _Vue;

    function vuexInit() {
        var options = this.$options;
        // store injection
        if (options.store) {
            this.$store = typeof options.store === 'function' ?
                options.store() :
                options.store;
        } else if (options.parent && options.parent.$store) {
            this.$store = options.parent.$store;
        }
    }
    Vue.mixin({
        beforeCreate: vuexInit
    });
}

我只知道,Vue.use()会默认执行js里面导出的install方法,然后将Vue对象作为参数传入install中

在这里,我们给vue的mixin全局混入,在每个创建vue对象之前beforeCreate,触发vuexInit方法。

vuexInit中this就是这个组件实例,然后他的options就是配置,如:

new Vue({
    store,
    render: h => h(App),
}).$mount('#app')
{
    store,
    render: h => h(App),
}

就是options,然后判断options上有没有store对象,如果有的话,就给this
创建一个$store属性,然后他的值做一个三元判断,有可能这个vuex并没有初始化,所以他是一个方法,我们就赋值这个运行后的方法,然后如果不是一个方法,说明store是初始化好的,我们直接赋值。

这样这个组件就能通过this.$store获取store。

这个情况是针对第一个vue组件的,而其他的子组件,我们就要通过options.parent获取到父组件,然后判断父组件有没有store,有的话就this.$store = options.parent.$store;

然后我没有看过vue的源码,但是根据这个情况,我判断vue的组件渲染他是有顺序的,所以这里总能从父组件获取到store,然后这样不断的传递下去,父传子,子传孙,然后他们都是同一个vuex对象。

完成品

main.js调用

import Vue from 'vue'
import Vuex from './min-vuex'
import App from './App.vue'

Vue.use(Vuex)
Vue.config.productionTip = false

const store = new Vuex.Store({
    state: {
        count: 0,
    },
    mutations: {
        increment(state) {
            state.count++
        }
    },
    getters: {
        doubleCount(state) {
            return state.count * 2
        }
    }
})
new Vue({
    store,
    render: h => h(App),
}).$mount('#app')

miniVuex

let Vue;

function install(_Vue) {
    Vue = _Vue;

    function vuexInit() {
        var options = this.$options;
        // store injection
        if (options.store) {
            this.$store = typeof options.store === 'function' ?
                options.store() :
                options.store;
        } else if (options.parent && options.parent.$store) {
            this.$store = options.parent.$store;
        }
    }
    Vue.mixin({
        beforeCreate: vuexInit
    });
}

const Store = function Store(options = {}) {
    const {
        state = {}, mutations = {}, getters = {}
    } = options
    const computed = {}
    const store = this
    store.getters = {};
    for (let [key, fn] of Object.entries(getters)) {
        computed[key] = function() {
            return fn(store.state, store.getters);
        };
        Object.defineProperty(store.getters, key, {
            get: function() {
                return store._vm[key];
            },
        });
    }
    this._vm = new Vue({
        data: {
            $$state: state
        },
        computed,
    })
    this._mutations = mutations
}
Store.prototype.commit = function(type, payload) {
    if (this._mutations[type]) {
        this._mutations[type](this.state, payload)
    }
}
Object.defineProperties(Store.prototype, {
    state: {
        get: function() {
            return this._vm._data.$$state
        }
    }
});
export default {
    Store,
    install
}
分类: vue 开发实战 标签: vuevuexdefineProperties

评论

暂无评论数据

暂无评论数据

目录