Vuex

vuex源码的解析应该算是我写的最详细的了,相比vue核心原理来说。

Vue.use

Vue.use 是vue扩展插件的方法, 扩展的插件必须有install方法(或者其本身是一个方法)。

vuex.install

Vue.use = function (plugin) {
  // 在Vue构造函数上的_installedPlugins属性是否包含vuex插件来判断是否重复安装
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []));
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }

    // additional parameters
    // 将除了插件本身外的参数构造到新数组中 作为vuex的参数
    const args = toArray(arguments, 1);
    // 这里用到了数组中不常用的添加数据到头部的方法,
    // 把vue构造函数添加到参数中 
    args.unshift(this);
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args);
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args);
    }
    installedPlugins.push(plugin);
    return this
  };
let Vue;
function install (_Vue) {
  if (Vue && _Vue === Vue) {
    {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      );
    }
    return
  }
  Vue = _Vue;
  applyMixin(Vue);
}

install的参数是Vue构造函数,为了避免重复载入vuex插件,在VUEX中预先设置一个变量,当第一次加载时把VUE构造函数赋给它,这样, 当多次加载插件时通过判断是否存在,避免多次加载插件。

applyMixin

  • 把vuex初始化操作注入到vue的beforeCreate阶段
/**
 * vuex v3.1.3
 */
function applyMixin (Vue) {
  const version = Number(Vue.version.split('.')[0]);

  if (version >= 2) {
    // vuex>2版本
    // vue.minin将beforeCreate发生的函数提前加载到vue构造函数中
    Vue.mixin({ beforeCreate: vuexInit });
  } else {
    // <2版本太旧 不考虑了
    // override init and inject vuex init procedure
    // for 1.x backwards compatibility.
    ...
  }

  /**
   * Vuex init hook, injected into each instances init hooks list.
   */

  function vuexInit () {
    const 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方法 把传入的参数合并到Vue参数中,随着实例生成而生成

Vue.mixin = function (mixin) {
    this.options = mergeOptions(this.options, mixin);
    return this
  };

当执行new Vue(...)生成实例后 就会将vuex 加载到其中。这样每个实例都会获取到。

使用vuex 必须用Vuex.Store方法传入对象

  • 对象有modules分模块
  • 每个模块有state getters actions mutations
const store = new Vuex.Store({
    modules: {
      home: {
        state: {}, 
        getters: {},
        actions: {},
        mutations: {}
      }
    }
  })

Vuex.Store

class Store {
  constructor (options = {}) {
    // Auto install if it is not done yet and `window` has `Vue`.
    // To allow users to avoid auto-installation in some cases,
    // this code should be placed here. See #731
    // 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); //递归调用参数中的modules生成树结构
    this._modulesNamespaceMap = Object.create(null); // 存储命名空间,方便后期直接取
    this._subscribers = [];
    this._watcherVM = new Vue(); // 生成vue实例对象,执行上面注入到beforeCreated中的vuexInit方法
    this._makeLocalGettersCache = Object.create(null);
  }
}
// 这样在每个vue实例中就会得到$store对象
 function vuexInit () {
    const options = this.$options;
    // store injection
    if (options.store) {
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store; // 这是根节点vue实例上的store
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store; //子组件的store会找父组件上的直到根节点store
    }
  }

根据path递归生成模块

动态注册

  • path如果是字符串则要转换为数组,如'module1' => ['module1'] 然后把模块注册到路径上
registerModule (path, rawModule, options = {}) {
    if (typeof path === 'string') path = [path];

    {
      assert(Array.isArray(path), `module path must be a string or an Array.`);
      assert(path.length > 0, 'cannot register the root module by using registerModule.');
    }

    this._modules.register(path, rawModule); //注册模块
    installModule(this, this.state, path, this._modules.get(path), options.preserveState);// 安装模块
    // reset store to update getters...
    resetStoreVM(this, this.state); // 重置模块
  }

注册模块

  • 总体逻辑:把传入的参数先转换成module,获取父模块,然后作为子模块插入到父模块中。
  • 获取父模块逻辑有点绕:如果path为空数组,那么为根模块。 除最后一个,则为模块的路径,如果数组为1,则模块路径在根路径下,也就是父模块为root 如果path不为空,那么该数组的最后一个为要注册的模块,前面的为路径。比如['child1', 'child1_1', 'child1_1_1']
  • 通过 this._modules.register 方法注册
  • 优秀代码:path.slice(0, -1) 以前获取数组的前几个都是 const arr = []; arr.slice(0, arr.length - 1) 倒数第一个为-1 倒数第二个为 -2 ,这样写很直观。
...
this._modules.register(path, rawModule);
... 
register (path, rawModule, runtime = true) {
    {
      assertRawModule(path, rawModule);
    }

    const newModule = new Module(rawModule, runtime);
    if (path.length === 0) {
      this.root = newModule;
    } else {
      const parent = this.get(path.slice(0, -1)); // 取路径的从0到倒数第二个作为新数组来获取父模块
      parent.addChild(path[path.length - 1], newModule);
    }

    // register nested modules
    // 递归调用子模块注册 
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime);
      });
    }
  }

安装模块

  • state:用 Vue.set 给模块的父模块的state对象添加该模块的state响应式属性。
function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length
  const namespace = store._modules.getNamespace(path)

  // register in namespace map
  if (module.namespaced) {
    if (store._modulesNamespaceMap[namespace] && "development" !== 'production') {
      console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`);
    }
    store._modulesNamespaceMap[namespace] = module;
  }

  // set state
  if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1));
    const moduleName = path[path.length - 1];
    store._withCommit(() => {
      {
        if (moduleName in parentState) {
          console.warn(
            `[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('.')}"`
          );
        }
      }
      Vue.set(parentState, moduleName, module.state); // 这是重点 用到了Vue的 set方法
    });
  }
// 构造执行环境
// 这部分暂时放过 过后再理解。
  const local = module.context = makeLocalContext(store, namespace, path);
 

  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key;
    registerMutation(store, namespacedType, mutation, local);
  });

  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key;
    const handler = action.handler || action;
    registerAction(store, type, handler, local);
  });

  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key;
    registerGetter(store, namespacedType, getter, local);
  });

  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot);
  });
}

vuex中的state获取API

// 使用方法 
// 找到模块然后直接获取state属性,而不是像dispatch commit 那样通过命名空间的方式
this.$store.home.a
// this.$store 代表root state
  • 安装模块的逻辑中 有一处 Vue.set(parentState, moduleName, module.state);
  • 找到父的state对象,然后用vue.set把该模块的state添加响应式属性
function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length;
  const namespace = store._modules.getNamespace(path);
...
  // set state
  if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1));
    const moduleName = path[path.length - 1];
    store._withCommit(() => {
      {
        if (moduleName in parentState) {
          console.warn(
            `[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('.')}"`
          );
        }
      }
      Vue.set(parentState, moduleName, module.state);
    });
  }

getters API

  • 使用方法 this.$store.getters['home/getterHome']
  • resetStoreVM 重置
  • 直接是通过getters根上获取该getter对应的key值, 这个key值是命名空间路径,代表的是唯一key
  • 使用了 Object.defineProperty 对 store.getters对象上生成以模块key,它的取值get方法是返回的store._vm[key]
  • store._vm 属性保存当前的vue实例,具体作用是data里面保存着state,computed存着getters,可以缓存。 故上面的取值getter获取的直接是缓存后的getter值
  • store._wrappedGetters 保存着当前的getters,是在installModule阶段生成
function resetStoreVM (store, state, hot) {
  const oldVm = store._vm;

  // bind store public getters
  store.getters = {};
  // reset local getters cache
  store._makeLocalGettersCache = Object.create(null);
  const wrappedGetters = store._wrappedGetters; // 注册的时候生成的
  const computed = {};
  forEachValue(wrappedGetters, (fn, key) => {
    // use computed to leverage its lazy-caching mechanism
    // direct inline function use will lead to closure preserving oldVm.
    // using partial to return function with only arguments preserved in closure environment.
    computed[key] = partial(fn, store);
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    });
  });

  // use a Vue instance to store the state tree
  // suppress warnings just in case the user has added
  // some funky global mixins
  const silent = Vue.config.silent;
  Vue.config.silent = true;
  store._vm = new Vue({// 用Vue实例对象来构造state和 getters 
    data: {
      $$state: state
    },
    computed
  });
  Vue.config.silent = silent;

  // enable strict mode for new vm
  if (store.strict) {
    enableStrictMode(store);
  }

  if (oldVm) {
    if (hot) {
      // dispatch changes in all subscribed watchers
      // to force getter re-evaluation for hot reloading.
      store._withCommit(() => {
        oldVm._data.$$state = null;
      });
    }
    Vue.nextTick(() => oldVm.$destroy());
  }
}

installModule 生成的getters

  • 故getters中定义的每个属性的执行方法的参数包括4个方面.
getters: {
  getA(state, getters, rootState, rootGetters) {
    .....
  }
}
  • 源码在此
function registerGetter (store, type, rawGetter, local) {
  if (store._wrappedGetters[type]) {
    {
      console.error(`[vuex] duplicate getter key: ${type}`);
    }
    return
  }
  store._wrappedGetters[type] = function wrappedGetter (store) {
    return rawGetter(
      local.state, // local state 当前模块
      local.getters, // local getters 当前模块
      store.state, // root state 根模块
      store.getters // root getters 根模块
    )
  };
}

mutations 原理及改变状态唯一操作的原因

  • 使用方法, 参数中有当前模块的state 以及传入的参数
mutations: {
  changeHomeVal(state, playload) {
    state.home = playload
  }
}
  • installModule阶段 注册
function registerMutation (store, type, handler, local) {
  const entry = store._mutations[type] || (store._mutations[type] = []);
  entry.push(function wrappedMutationHandler (payload) {
    handler.call(store, local.state, payload); // 传入当前模块的 state 以及输入参数, 可以看到每个是数组
  });
}
  • 获取注册到store._mutations对应的mutation, 通过store.commit方法执行。
commit (_type, _payload, _options) {
    // check object-style commit
   ...
   const entry = this._mutations[type];
   ...
    this._withCommit(() => {
      entry.forEach(function commitIterator (handler) {
        handler(payload);
      });
    });
   ...
  • 对于为什么commit是状态的唯一方法, 并且是同步函数,不然下面的_committing 状态起不到作用
_withCommit (fn) {
    const committing = this._committing;
    this._committing = true;
    fn();
    this._committing = committing;
  }

actions

  • 使用方法
actions: {
      getHome({ state, getters, commit }, payload) {
        return new Promise((resolve, reject) => {
          commit('matution', 3)
          resolve(3)
        })
      },
    },
  • 注册阶段, 获取到store._actions下的action, 也是个数组
  • 执行的时候传入的参数有一个包含当前的dispatch commit getters state 根getters state的对象,可以通过解构方法获取
  • actions返回的结果为promise 不是的话 通过Promise.resolve()构造之。
function registerAction (store, type, handler, local) {
  const entry = store._actions[type] || (store._actions[type] = []);
  entry.push(function wrappedActionHandler (payload) {
    let res = handler.call(store, {
      dispatch: local.dispatch,
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload);
    if (!isPromise(res)) {
      res = Promise.resolve(res);
    }
    ...
  });
}