阅读 312

vuex源码解析

vuex简介

能看到此文章的人,应该大部分都已经使用过vuex了,想更深一步了解vuex的内部实现原理。所以简介就少介绍一点。官网介绍说Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。数据流的状态非常清晰,按照 组件dispatch Action -> action内部commit Mutation -> Mutation再 mutate state 的数据,在触发render函数引起视图的更新。附上一张官网的流程图及vuex的官网地址:vuex.vuejs.org/zh/

Questions

在使用vuex的时候,大家有没有如下几个疑问,带着这几个疑问,再去看源码,从中找到解答,这样对vuex的理解可以加深一些。

  1. 官网在严格模式下有说明:在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。vuex是如何检测状态改变是由mutation函数引起的?
  2. 通过在根实例中注册 store 选项,该 store 实例会注入到根组件下的所有子组件中。为什么所有子组件都可以取到store?
  3. 为什么用到的属性在state中也必须要提前定义好,vue视图才可以响应?
  4. 在调用dispatch和commit时,只需传入(type, payload),为什么action函数和mutation函数能够在第一个参数中解构出来state、commit等? 带着这些问题,我们来看看vuex的源码,从中寻找到答案。

源码目录结构

vuex的源码结构非常简洁清晰,代码量也不是很大,大家不要感到恐慌。

vuex挂载

vue使用插件的方法很简单,只需Vue.use(Plugins),对于vuex,只需要Vue.use(Vuex)即可。在use 的内部是如何实现插件的注册呢?读过vue源码的都知道,如果传入的参数有 install 方法,则调用插件的 install 方法,如果传入的参数本身是一个function,则直接执行。那么我们接下来就需要去 vuex 暴露出来的 install 方法去看看具体干了什么。

store.js

export function install(_Vue) {
  // vue.use原理:调用插件的install方法进行插件注册,并向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; // 为了引用vue的watch方法
  applyMixin(Vue);
}
复制代码

在 install 中,将 vue 对象赋给了全局变量 Vue,并作为参数传给了 applyMixin 方法。那么在 applyMixin 方法中干了什么呢?

mixin.js

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 的版本,2以上的版本把 vuexInit 函数混入 vuex 的 beforeCreate 钩子函数中。 在 vuexInit 中,将 new Vue() 时传入的 store 设置到 this 对象的 $store 属性上,子组件则从其父组件上引用其 $store 属性进行层层嵌套设置,保证每一个组件中都可以通过 this.$store 取到 store 对象。 这也就解答了我们问题 2 中的问题。通过在根实例中注册 store 选项,该 store 实例会注入到根组件下的所有子组件中,注入方法是子从父拿,root从options拿。

接下来让我们看看new Vuex.Store()都干了什么。

store构造函数

store对象构建的主要代码都在store.js中,是vuex的核心代码。

首先,在 constructor 中进行了 Vue 的判断,如果没有通过 Vue.use(Vuex) 进行 Vuex 的注册,则调用 install 函数注册。( 通过 script 标签引入时不需要手动调用 Vue.use(Vuex) ) 并在非生产环境进行判断: 必须调用 Vue.use(Vuex) 进行注册,必须支持 Promise,必须用 new 创建 store。

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.`
    );
}
复制代码

然后进行一系列的属性初始化。其中的重点是 new ModuleCollection(options),这个我们放在后面再讲。先把 constructor 中的代码过完。

const { plugins = [], strict = false } = options;

// store internal state
this._committing = false; // 是否在进行提交mutation状态标识
this._actions = Object.create(null); // 保存action,_actions里的函数已经是经过包装后的
this._actionSubscribers = []; // action订阅函数集合
this._mutations = Object.create(null); // 保存mutations,_mutations里的函数已经是经过包装后的
this._wrappedGetters = Object.create(null); // 封装后的getters集合对象
// Vuex支持store分模块传入,在内部用Module构造函数将传入的options构造成一个Module对象,
// 如果没有命名模块,默认绑定在this._modules.root上
// ModuleCollection 内部调用 new Module构造函数
this._modules = new ModuleCollection(options);
this._modulesNamespaceMap = Object.create(null); // 模块命名空间map
this._subscribers = []; // mutation订阅函数集合
this._watcherVM = new Vue(); // Vue组件用于watch监视变化
复制代码

属性初始化完毕后,首先从 this 中解构出原型上的 dispatchcommit 方法,并进行二次包装,将 this 指向当前 store。

const store = this;
const { dispatch, commit } = this;
/**
 把 Store 类的 dispatch 和 commit 的方法的 this 指针指向当前 store 的实例上. 
 这样做的目的可以保证当我们在组件中通过 this.$store 直接调用 dispatch/commit 方法时, 
 能够使 dispatch/commit 方法中的 this 指向当前的 store 对象而不是当前组件的 this.
*/
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);
};

复制代码

接着往下走,包括严格模式的设置、根state的赋值、模块的注册、state的响应式、插件的注册等等,其中的重点在 installModule 函数中,在这里实现了所有modules的注册。

//options中传入的是否启用严格模式
this.strict = strict;

// new ModuleCollection 构造出来的_mudules
const state = this._modules.root.state;

// 初始化组件树根组件、注册所有子组件,并将其中所有的getters存储到this._wrappedGetters属性中
installModule(this, state, [], this._modules.root);

//通过使用vue实例,初始化 store._vm,使state变成可响应的,并且将getters变成计算属性
resetStoreVM(this, state);

// 注册插件
plugins.forEach(plugin => plugin(this));

// 调试工具注册
const useDevtools =
    options.devtools !== undefined ? options.devtools : Vue.config.devtools;
if (useDevtools) {
   devtoolPlugin(this);
}
复制代码

到此为止,constructor 中所有的代码已经分析完毕。其中的重点在 new ModuleCollection(options)installModule ,那么接下来我们到它们的内部去看看,究竟都干了些什么。

ModuleCollection

由于 Vuex 使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。Vuex 允许我们将 store 分割成模块(module),每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块。例如下面这样:

const childModule = {
  state: { ... },
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  state,
  getters,
  actions,
  mutations,
  modules: {
    childModule: childModule,
  }
})

复制代码

有了模块的概念,可以更好的规划我们的代码。对于各个模块公用的数据,我们可以定义一个common store,别的模块用到的话直接通过 modules 的方法引入即可,无需重复的在每一个模块都写一遍相同的代码。这样我们就可以通过 store.state.childModule 拿到childModule中的 state 状态, 对于Module的内部是如何实现的呢?

export default class ModuleCollection {
  constructor(rawRootModule) {
    // 注册根module,参数是new Vuex.Store时传入的options
    this.register([], rawRootModule, false);
  }

  register(path, rawModule, runtime = true) {
    if (process.env.NODE_ENV !== "production") {
      assertRawModule(path, rawModule);
    }

    const newModule = new Module(rawModule, runtime);
    if (path.length === 0) {
      // 注册根module
      this.root = newModule;
    } else {
      // 注册子module,将子module添加到父module的_children属性上
      const parent = this.get(path.slice(0, -1));
      parent.addChild(path[path.length - 1], newModule);
    }

    // 如果当前模块有子modules,循环注册
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime);
      });
    }
  }
}
复制代码

在ModuleCollection中又调用了Module构造函数,构造一个Module。

Module构造函数

constructor (rawModule, runtime) {
    // 初始化时为false
    this.runtime = runtime
    // 存储子模块
    this._children = Object.create(null)
    // 将原来的module存储,以备后续使用
    this._rawModule = rawModule
    const rawState = rawModule.state
    // 存储原来module的state
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }
复制代码

通过以上代码可以看出,ModuleCollection 主要将传入的 options 对象整个构造为一个 Module 对象,并循环调用 this.register([key], rawModule, false) 为其中的 modules 属性进行模块注册,使其都成为 Module 对象,最后 options 对象被构造成一个完整的 Module 树。

经过 ModuleCollection 构造后的树结构如下:(以上面的例子生成的树结构)

模块已经创建好之后,接下来要做的就是 installModule。

installModule

首先我们来看一看执行完 constructor 中的 installModule 函数后,这棵树的结构如何?

从上图中可以看出,在执行完installModule函数后,每一个 module 中的 state 属性都增加了 其子 module 中的 state 属性,但此时的 state 还不是响应式的,并且新增加了 context 这个对象。里面包含 dispatch 、 commit 等函数以及 state 、 getters 等属性。它就是 vuex 官方文档中所说的Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象 这个 context 对象。我们平时在 store 中调用的 dispatch 和 commit 就是从这里解构出来的。接下来让我们看看 installModule 里面执行了什么。

function installModule(store, rootState, path, module, hot) {
  // 判断是否是根节点,跟节点的path = []
  const isRoot = !path.length;

  // 取命名空间,形式类似'childModule/'
  const namespace = store._modules.getNamespace(path);

  // 如果namespaced为true,存入_modulesNamespaceMap中
  if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = module;
  }

  // 不是根节点,把子组件的每一个state设置到其父级的state属性上
  if (!isRoot && !hot) {
    // 获取当前组件的父组件state
    const parentState = getNestedState(rootState, path.slice(0, -1));
    // 获取当前Module的名字
    const moduleName = path[path.length - 1];
    store._withCommit(() => {
      Vue.set(parentState, moduleName, module.state);
    });
  }

  // 给context对象赋值
  const local = (module.context = makeLocalContext(store, namespace, path));

  // 循环注册每一个module的Mutation
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key;
    registerMutation(store, namespacedType, mutation, local);
  });

  // 循环注册每一个module的Action
  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key;
    const handler = action.handler || action;
    registerAction(store, type, handler, local);
  });

  // 循环注册每一个module的Getter
  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key;
    registerGetter(store, namespacedType, getter, local);
  });

  // 循环_childern属性
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot);
  });
}
复制代码

在installModule函数里,首先判断是否是根节点、是否设置了命名空间。在设置了命名空间的前提下,把 module 存入 store._modulesNamespaceMap 中。在不是跟节点并且不是 hot 的情况下,通过 getNestedState 获取到父级的 state,并获取当前 module 的名字, 用 Vue.set() 方法将当前 module 的 state 挂载到父 state 上。然后调用 makeLocalContext 函数给 module.context 赋值,设置局部的 dispatch、commit方法以及getters和state。那么来看一看这个函数。

function makeLocalContext(store, namespace, path) {
  // 是否有命名空间
  const noNamespace = namespace === "";

  const local = {
    // 如果没有命名空间,直接返回store.dispatch;否则给type加上命名空间,类似'childModule/'这种
    dispatch: noNamespace
      ? store.dispatch
      : (_type, _payload, _options) => {
          const args = unifyObjectStyle(_type, _payload, _options);
          const { payload, options } = args;
          let { type } = args;

          if (!options || !options.root) {
            type = namespace + type;
            if (
              process.env.NODE_ENV !== "production" &&
              !store._actions[type]
            ) {
              console.error(
                `[vuex] unknown local action type: ${
                  args.type
                }, global type: ${type}`
              );
              return;
            }
          }

          return store.dispatch(type, payload);
        },
    // 如果没有命名空间,直接返回store.commit;否则给type加上命名空间
    commit: noNamespace
      ? store.commit
      : (_type, _payload, _options) => {
          const args = unifyObjectStyle(_type, _payload, _options);
          const { payload, options } = args;
          let { type } = args;

          if (!options || !options.root) {
            type = namespace + type;
            if (
              process.env.NODE_ENV !== "production" &&
              !store._mutations[type]
            ) {
              console.error(
                `[vuex] unknown local mutation type: ${
                  args.type
                }, global type: ${type}`
              );
              return;
            }
          }

          store.commit(type, payload, options);
        }
  };

  // getters and state object must be gotten lazily
  // because they will be changed by vm update
  Object.defineProperties(local, {
    getters: {
      get: noNamespace
        ? () => store.getters
        : () => makeLocalGetters(store, namespace)
    },
    state: {
      get: () => getNestedState(store.state, path)
    }
  });

  return local;
}
复制代码

经过 makeLocalContext 处理的返回值会赋值给 local 变量,这个变量会传递给 registerMutation、forEachAction、registerGetter 函数去进行相应的注册。

mutation可以重复注册,registerMutation 函数将我们传入的 mutation 进行了一次包装,将 state 作为第一个参数传入,因此我们在调用 mutation 的时候可以从第一个参数中取到当前的 state 值。

function registerMutation(store, type, handler, local) {
  const entry = store._mutations[type] || (store._mutations[type] = []);
  entry.push(function wrappedMutationHandler(payload) {
    // 将this指向store,将makeLocalContext返回值中的state作为第一个参数,调用值执行的payload作为第二个参数
    // 因此我们调用commit去提交mutation的时候,可以从mutation的第一个参数中取到当前的state值。
    handler.call(store, local.state, payload);
  });
}
复制代码

action也可以重复注册。注册 action 的方法与 mutation 相似,registerAction 函数也将我们传入的 action 进行了一次包装。但是 action 中参数会变多,里面包含 dispatch 、commit、local.getters、local.state、rootGetters、rootState,因此可以在一个 action 中 dispatch 另一个 action 或者去 commit 一个 mutation。这里也就解答了问题4中提出的疑问。

function registerAction(store, type, handler, local) {
  const entry = store._actions[type] || (store._actions[type] = []);
  entry.push(function wrappedActionHandler(payload, cb) {
    //与mutation不同,action的第一个参数是一个对象,里面包含dispatch、commit、getters、state、rootGetters、rootState
    let res = handler.call(
      store,
      {
        dispatch: local.dispatch,
        commit: local.commit,
        getters: local.getters,
        state: local.state,
        rootGetters: store.getters,
        rootState: store.state
      },
      payload,
      cb
    );
    if (!isPromise(res)) {
      res = Promise.resolve(res);
    }
    if (store._devtoolHook) {
      return res.catch(err => {
        store._devtoolHook.emit("vuex:error", err);
        throw err;
      });
    } else {
      return res;
    }
  });
}
复制代码

注册 getters,从getters的第一个参数中可以取到local state、local getters、root state、root getters。getters不允许重复注册。

function registerGetter(store, type, rawGetter, local) {
  // getters不允许重复
  if (store._wrappedGetters[type]) {
    if (process.env.NODE_ENV !== "production") {
      console.error(`[vuex] duplicate getter key: ${type}`);
    }
    return;
  }

  store._wrappedGetters[type] = function wrappedGetter(store) {
    // getters的第一个参数包含local state、local getters、root state、root getters
    return rawGetter(
      local.state, // local state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    );
  };
}
复制代码

现在 store 的 _mutation、_action 中已经有了我们自行定义的的 mutation 和 action函数,并且经过了一层内部报装。当我们在组件中执行 this.$store.dispatch()this.$store.commit() 的时候,是如何调用到相应的函数的呢?接下来让我们来看一看 store 上的 dispatch 和 commit 函数。

commit

commit 函数先进行参数的适配处理,然后判断当前 action type 是否存在,如果存在则调用 _withCommit 函数执行相应的 mutation 。

  // 提交mutation函数
  commit(_type, _payload, _options) {
    // check object-style commit
    //commit支持两种调用方式,一种是直接commit('getName','vuex'),另一种是commit({type:'getName',name:'vuex'}),
    //unifyObjectStyle适配两种方式
    const { type, payload, options } = unifyObjectStyle(
      _type,
      _payload,
      _options
    );

    const mutation = { type, payload };
    // 这里的entry取值就是我们在registerMutation函数中push到_mutations中的函数,已经经过处理
    const entry = this._mutations[type];
    if (!entry) {
      if (process.env.NODE_ENV !== "production") {
        console.error(`[vuex] unknown mutation type: ${type}`);
      }
      return;
    }

    // 专用修改state方法,其他修改state方法均是非法修改,在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误
    // 不要在发布环境下启用严格模式!严格模式会深度监测状态树来检测不合规的状态变更——请确保在发布环境下关闭严格模式,以避免性能损失。
    this._withCommit(() => {
      entry.forEach(function commitIterator(handler) {
        handler(payload);
      });
    });

    // 订阅者函数遍历执行,传入当前的mutation对象和当前的state
    this._subscribers.forEach(sub => sub(mutation, this.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"
      );
    }
  }
复制代码

在 commit 函数中调用了 _withCommit 这个函数, 代码如下。 _withCommit 是一个代理方法,所有触发 mutation 的进行 state 修改的操作都经过它,由此来统一管理监控 state 状态的修改。在严格模式下,会深度监听 state 的变化,如果没有通过 mutation 去修改 state,则会报错。官方建议 不要在发布环境下启用严格模式! 请确保在发布环境下关闭严格模式,以避免性能损失。这里就解答了问题1中的疑问。

_withCommit(fn) {
    // 保存之前的提交状态false
    const committing = this._committing;

    // 进行本次提交,若不设置为true,直接修改state,strict模式下,Vuex将会产生非法修改state的警告
    this._committing = true;

    // 修改state
    fn();

    // 修改完成,还原本次修改之前的状态false
    this._committing = committing;
}
复制代码

dispatch

dispatch 和 commit 的原理相同。如果有多个同名 action,会等到所有的 action 函数完成后,返回的 Promise 才会执行。

// 触发action函数
  dispatch(_type, _payload) {
    // check object-style dispatch
    const { type, payload } = unifyObjectStyle(_type, _payload);

    const action = { type, payload };
    const entry = this._actions[type];
    if (!entry) {
      if (process.env.NODE_ENV !== "production") {
        console.error(`[vuex] unknown action type: ${type}`);
      }
      return;
    }

    // 执行所有的订阅者函数
    this._actionSubscribers.forEach(sub => sub(action, this.state));

    return entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload);
  }
复制代码

至此,整个 installModule 里涉及到的内容已经分析完毕。现在我们来看一看store树结构。

我们在 options 中传进来的 action 和 mutation 已经在 store 中。但是 state 和 getters 还没有。这就是接下来的 resetStoreVM 方法做的事情。

resetStoreVM

resetStoreVM 函数中包括初始化 store._vm,观测 state 和 getters 的变化以及执行是否开启严格模式等。state 属性赋值给 vue 实例的 data 属性,因此数据是可响应的。这也就解答了问题 3,用到的属性在 state 中也必须要提前定义好,vue 视图才可以响应。

function resetStoreVM(store, state, hot) {
  //保存老的vm
  const oldVm = store._vm;

  // 初始化 store 的 getters
  store.getters = {};

  // _wrappedGetters 是之前在 registerGetter 函数中赋值的
  const wrappedGetters = store._wrappedGetters;

  const computed = {};
  
  forEachValue(wrappedGetters, (fn, key) => {
    // 将getters放入计算属性中,需要将store传入
    computed[key] = () => fn(store);
    // 为了可以通过this.$store.getters.xxx访问getters
    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
  // 用一个vue实例来存储store树,将getters作为计算属性传入,访问this.$store.getters.xxx实际上访问的是store._vm[xxx]
  const silent = Vue.config.silent;
  Vue.config.silent = true;
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  });
  Vue.config.silent = silent;

  // enable strict mode for new vm
  // 如果是严格模式,则启用严格模式,深度 watch state 属性
  if (store.strict) {
    enableStrictMode(store);
  }

  // 若存在oldVm,解除对state的引用,等dom更新后把旧的vue实例销毁
  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());
  }
}
复制代码

开启严格模式时,会深度监听 $$state 的变化,如果不是通过this._withCommit()方法触发的state修改,也就是store._committing如果是false,就会报错。

function enableStrictMode(store) {
  store._vm.$watch(
    function() {
      return this._data.$$state;
    },
    () => {
      if (process.env.NODE_ENV !== "production") {
        assert(
          store._committing,
          `do not mutate vuex store state outside mutation handlers.`
        );
      }
    },
    { deep: true, sync: true }
  );
}
复制代码

让我们来看一看执行完 resetStoreVM 后的 store 结构。现在的 store 中已经有了 getters 属性,并且 getters 和 state 都是响应式的。

至此 vuex 的核心代码初始化部分已经分析完毕。源码里还包括一些插件的注册及暴露出来的 API 像 mapState mapGetters mapActions mapMutation等函数就不在这里介绍了,感兴趣的可以自行去源码里看看,比较好理解。这里就不做过多介绍。

总结

vuex的源码相比于vue的源码来说还是很好理解的。分析源码之前建议大家再细读一遍官方文档,遇到不太理解的地方记下来,带着问题去读源码,有目的性的研究,可以加深记忆。阅读的过程中,可以先写一个小例子,引入 clone 下来的源码,一步一步分析执行过程。

关注下面的标签,发现更多相似文章
评论