vuex源码解读&简易实现

2020-02-06  本文已影响0人  唐井儿_

源码解读

开始之前,先贴个大纲:

首先,我们从使用方法入手,一步步来看

// store.js
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export default new Vuex.Store({
    state: {
        msg: ''
    },
    getters: {
        msgPlus: state => state.msg + ' plus'
    },
    mutations: {
        setMsg (store, data) {
            store.msg = data;
        }
    },
    actions: {
        asyncChangeMsg ({commit}, data) {
            setTimeout(() => {
                commit('setMsg', data);
            }, 2000)
        }
    }
})


// main.js
import Vue from 'vue';
import App from 'App.vue';
import store from './store';

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


// App.vue
<template>
    <div>
        msg: {{$store.state.msg}} <br />
        msgPlus: {{$store.getters.msgPlus}}
    </div>
</template>

<script>
export default {
    created () {
        this.$store.dispatch('asyncChangeMsg', 22)
    }
}
</script>

接下来

  1. 从store.js文件入手。Vue.use(Vuex): 据官网介绍,此方法用来安装插件,插件必须提供install方法,且会将Vue作为参数传入;
  2. 于是,查看导出vuex的地方,找到./node_modules/vuex/dist/vuex.esm.js,就是源码部分了。注:后缀esm(EcmaScript Module)表示es6模块版;
  3. 此文件底部,查看导出部分,确存在install方法
var index_esm = {
  Store: Store,
  install: install,
  version: '3.1.2',
  mapState: mapState,
  mapMutations: mapMutations,
  mapGetters: mapGetters,
  mapActions: mapActions,
  createNamespacedHelpers: createNamespacedHelpers
};

export default index_esm;
  1. 查看install实现,传入的参数是真正的Vue
var Vue; // bind on install
function install (_Vue) {
  // 防止重复 
  if (Vue && _Vue === Vue) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      );
    }
    return
  }
  Vue = _Vue;
  applyMixin(Vue);
}
  1. 上述代码段先是防止重复调用Vue.use方法,随后调用applyMixin方法
function applyMixin (Vue) {
  var version = Number(Vue.version.split('.')[0]);
  if (version >= 2) {
    // 在每个Vue实例的beforeCreated生命周期时,执行vuexInit方法
    Vue.mixin({ beforeCreate: vuexInit });      
  } else {
      // ...
  }
}
  1. 上述代码段,根据不同vue版本分别处理,这里暂且只看2.x及以上版本。
    Vue.mixin方法:据官网介绍,全局混入,影响注册后创建的每个Vue实例。此处作用是,在每个Vue实例的beforeCreated生命周期时,执行vuexInit方法。
    下边看vuexInit方法做了什么?
function vuexInit () {
  // this表示该Vue实例,$options是实例初始化选项
  var options = this.$options;
  // store injection
  // 从options中取store,若存在,则赋值到this.$store上;否则,从父级取得$store进行赋值
  // this.$options.parent与this.$parent等价
  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;
  }
}
  1. 上述代码段,从main.js入口实例Vue时,初始化传入的store开始,一级级传到子组件,于是每个组件上都有了$store属性。
    于是,我们可以在每个组件上,通过this.$store进行操作,例如App.js中的this.$store.state.msg/this.$store.dispatch等

了解了Vue.use(Vuex)之后,再次回到store.js文件,接下来看实例化Store对象过程中发生了什么?注意一点:

// 成员访问的优先级高于new,于是先执行Vuex.Store,而后执行实例化
new Vuex.Store({})
  1. 首先贴出Store方法的定义:
var Store = function Store (options) {
  var this$1 = this;
  if ( options === void 0 ) options = {};

  if (!Vue && typeof window !== 'undefined' && window.Vue) {
    install(window.Vue);
  }

  if (process.env.NODE_ENV !== 'production') {
    assert(Vue, "must call Vue.use(Vuex) before creating a store instance.");
    assert(typeof Promise !== 'undefined', "vuex requires a Promise polyfill in this browser.");
    assert(this instanceof Store, "store must be called with the new operator.");
  }

  var plugins = options.plugins; if ( plugins === void 0 ) plugins = [];
  var strict = options.strict; if ( strict === void 0 ) strict = false;

  // store internal state
  this._committing = false;
  this._actions = Object.create(null);
  this._actionSubscribers = [];
  this._mutations = Object.create(null);
  this._wrappedGetters = Object.create(null);
  this._modules = new ModuleCollection(options);
  this._modulesNamespaceMap = Object.create(null);
  this._subscribers = [];
  this._watcherVM = new Vue();
  this._makeLocalGettersCache = Object.create(null);

  // bind commit and dispatch to self
  var store = this;
  var ref = this;
  var dispatch = ref.dispatch;
  var commit = ref.commit;
  this.dispatch = function boundDispatch (type, payload) {
    return dispatch.call(store, type, payload)
  };
  this.commit = function boundCommit (type, payload, options) {
    return commit.call(store, type, payload, options)
  };

  // strict mode
  this.strict = strict;

  var state = this._modules.root.state;
  
  // 模块相关
  installModule(this, state, [], this._modules.root);

  resetStoreVM(this, state);

  // apply plugins
  plugins.forEach(function (plugin) { return plugin(this$1); });

  var useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools;
  if (useDevtools) {
    devtoolPlugin(this);
  }
};
  1. installModule方法为模块相关,暂且不看,先来看看比较重要的resetStoreVM方法:
function resetStoreVM (store, state, hot) {
  var oldVm = store._vm;

  // bind store public getters
  store.getters = {};
  // reset local getters cache
  store._makeLocalGettersCache = Object.create(null);
  
  // _wrappedGetters会收集所有getters方法 -- mynote
  var wrappedGetters = store._wrappedGetters;
  
  // 将getters的方法包装一层后,收集到computed对象中 -- mynote
  // 使用Object.defineProperty注册store.getters,使得每次取值时,从store._vm中取。原因是下方进行new Vue传入computed后,computed的值会放到实例上边 -- mynote
  var computed = {};
  forEachValue(wrappedGetters, function (fn, key) {
    computed[key] = partial(fn, store);
    Object.defineProperty(store.getters, key, {
      get: function () { return store._vm[key]; },
      enumerable: true // for local getters
    });
  });

  var silent = Vue.config.silent;
  Vue.config.silent = true;
  
  // 将state值 和 getters值分别传入一个new Vue中初始化成data和computed,使其具有响应式特性    -- mynote
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed: computed
  });
  // ...
}
  1. 参考mynote部分,总的来说,此方法的作用是,借用Vue,使得state值和getters值具有响应式特征

再来看看,commit是如何触发mutations的?

Store.prototype.commit = function commit (_type, _payload, _options) {
  var this$1 = this;
    
  // check object-style commit
  var ref = unifyObjectStyle(_type, _payload, _options);
    var type = ref.type;
    var payload = ref.payload;
    var options = ref.options;

  var mutation = { type: type, payload: payload };
  var entry = this._mutations[type];
  if (!entry) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(("[vuex] unknown mutation type: " + type));
    }
    return
  }
  // 批量触发mutation
  this._withCommit(function () {
    entry.forEach(function commitIterator (handler) {
      handler(payload);
    });
  });
  // 通知订阅者
  this._subscribers.forEach(function (sub) { return sub(mutation, this$1.state); });

  if (
    process.env.NODE_ENV !== 'production' &&
    options && options.silent
  ) {
    console.warn(
      "[vuex] mutation type: " + type + ". Silent option has been removed. " +
      'Use the filter functionality in the vue-devtools'
    );
  }
};

简易实现

// src下,新建vuex.js文件,之前引入vuex的地方改为引用此文件,其他地方无需修改
let Vue;

class Store {
  constructor (options) {
    this.vm = new Vue({
      data: {state: options.state}
    })
    this.state = this.vm.state;
    this.mutations = options.mutations;
    this.actions = options.actions;
  }
  commit (eventName, data) {
    this.mutations[eventName](this.state, data)
  }
  dispatch (eventName, data) {
    this.actions[eventName](this, data)
  }
}

const install = (_Vue) => {
  Vue = _Vue;
  Vue.mixin({
    beforeCreate () {
      if (this.$options && this.$options.store) {
        this.$store = this.$options.store;
      } else {
        this.$store = this.$parent && this.$parent.$store;
      }
    }
  })
}

export default {
  Store,
  install
}

完整项目可参考

趁着空闲,记录下学习的知识,有待进一步完善...

上一篇下一篇

猜你喜欢

热点阅读