Vuex3源码学习笔记之Vuex的初始化

Store.js 文件中的代码中,会导出一个 Store 类,我们在使用 Vuex 的时候都是从先创建一个 Store 实例对象开始的。 下面看下 Store 实例对象的初始化过程。 先看下 Store 类的初始化函数 constructo

目录
  1. Store 的初始化
  2. 模块收集
    1. ModuleCollection模块收集类
    2. Module模块类
  3. 模块安装
    1. makeLocalContext创建模块上下文
  4. 初始化store.vm
  5. 模块收集
    1. ModuleCollection模块收集类
    2. Module模块类
  6. 模块安装
    1. makeLocalContext创建模块上下文
  7. 初始化store.vm
  8. 参考资料

Store.js文件中的代码中,会导出一个Store类,我们在使用 Vuex 的时候都是从先创建一个Store实例对象开始的。

下面看下Store实例对象的初始化过程。

Store 的初始化#

先看下Store类的初始化函数constructor的代码。

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
  // ! 使用 Vuex 为引入外链时,会自动安装插件
  if (!Vue && typeof window !== 'undefined' && window.Vue) {
    install(window.Vue)
  }
  // ...
}

在我们使用外链来引入 Vuex 时,会自动安装 Vuex,不需要再手动调用use方法来安装。

在我们使用script标签使用 Vuex 的时候,此时 Vue 库的代码必须先引入,然后再引入 Vuex 库的代码,这样才能使用 Vuex。在用外链引入 Vue 和 Vue 的代码时, Vue 的构造函数会被赋值给window.Vue且浏览器对象window也是有定义的,store.js文件中的全局变量Vue也还没有赋值,此时才会安装 Vuex。

示例代码如下

<body>
  <div id="app"></div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.js"></script>
  <script src="https://cdn.bootcss.com/vuex/3.1.1/vuex.js"></script>
  <script>
    var store = new Vuex.Store({
      state: {
        count: 0,
      },
      mutations: {
        addOne(state) {
          state.count += 1;
        },
      },
    });
 
    new Vue({
      template: `<div v-on:click="$store.commit('addOne')">count: {{$store.state.count}}</div>`,
      store,
    }).$mount("#app");
  </script>
</body>

继续看下面的代码。,是在开发环境中,执行三个断言

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.`);
}

先看下断言函数assert的代码,在util.js文件中

// ! 断言:当条件没达成的时候,抛出错误
export function assert(condition, msg) {
  if (!condition) throw new Error(`[vuex] ${msg}`);
}

这个函数很简单,没有满足断言条件的话,就会抛出错误。

下面分析下三个断言:

  • Vue 必须有值(存在),因为 Vuex 只有依赖 Vue 才能使用。
  • 必须是支持 Promise 的环境,因为 Vuex 中的 actions 使用了 Promise 语法来处理异步。
  • 传入到 Vue 的选项中的store实例对象必须是通过操作符 new 出来的Store实例。

继续看下面的代码。

// ! 获取选项中的插件(默认是空数组)和严格模式定义(默认是 false)
const { plugins = [], strict = false } = options;
 
// store internal state
this._committing = false; // ! 判断是否使用 commit 修改数据
this._actions = Object.create(null); // ! 存储 actions
this._actionSubscribers = []; // ! 存储 action 的所有订阅函数
this._mutations = Object.create(null); // ! 存储 mutations
this._wrappedGetters = Object.create(null); // ! 存储 wrapper getters
this._modules = new ModuleCollection(options); // ! ① 模块收集 => { root: rootModule }
this._modulesNamespaceMap = Object.create(null); // ! 模块命名映射表 { 'moduleName/': module}
this._subscribers = []; // ! 存储 mutation 的所有订阅函数
this._watcherVM = new Vue(); // ! 创建一个 Vue 实例,用来使用实例属性 $watch 实现 watch API
this._makeLocalGettersCache = Object.create(null); // ! 模块的 getters
 
// ...

options获取插件和strict模式。然后定义了一堆的属性,属性解析请看上面的注释。

这里主要看下this._modules属性,它是ModuleCollection类的实例对象,通过创建这个实例对象,进行模块的收集。

this._modules = new ModuleCollection(options); // ! ① 模块收集 => { root: rootModule }

这里是Store实例初始化的第一个重点阶段:模块收集。

模块收集#

ModuleCollection模块收集类#

查看ModuleCollection类的代码,在module/module-collection.js文件中

// ! 模块收集类,设置 root 模块
export default class ModuleCollection {
  constructor(rawRootModule) {
    // register root module (Vuex.Store options)
    this.register([], rawRootModule, false); // ! 初始化时注册模块
  }
 
  // ...
}

这个类的主要作用是收集模块,它会创建一个{ root: Module instance}实例对象。看类的构造函数,在初始化时只调用register方法来注册模块。

查看register方法的代码。

register(path, rawModule, runtime = true) {
  // ! 开发模式下断言原始数据,判断输入的数据类型和格式是否有错
  if (process.env.NODE_ENV !== 'production') {
    assertRawModule(path, rawModule)
  }
 
  const newModule = new Module(rawModule, runtime) // ! 创建一个模块
 
  // ! path 为空时
  if (path.length === 0) {
    this.root = newModule // ! 创建的模块为根模块,注意:this.root 是这里类唯一的属性值
  } else {
    const parent = this.get(path.slice(0, -1)) // ! 根据路径获取到父模块(在数组里面它前面的元素)
    parent.addChild(path[path.length - 1], newModule) // ! 添加子模块,建立父子关系
  }
 
  // register nested modules
  // ! 用户自定义模块,注册嵌套模块
  if (rawModule.modules) {
    forEachValue(rawModule.modules, (rawChildModule, key) => {
      // ! 把 key 放入到 path 中,key === moduleName
      this.register(path.concat(key), rawChildModule, runtime)
    })
  }
}

首先在开发环境中会使用断言函数校验用户输入的原始选项数据的类型或者格式是否有错。

然后声明常量newModule,存储通过操作符 new 创建一个Module实例对象的。先不管生成的实例对象是怎么样子的,继续看下面的代码。。

如果path数组的长度为 0 时,生成的模块被赋值给this.root,即生成根模块。注意:root属性是类ModuleCollection实例对象的唯一一个的属性值。我们在初始化模块时,path是一个空数组,所有先不看 else 代码块的代码,后面再解析。

继续看下面的代码。,如果我们的原始数据中设置了模块,会遍历这些模块,然后还是通过register方法注册这些模块。这里注册时,path就不是一个空数组了,而是合并了key,也就是模块名。这时,会进入上面的 else 代码块的逻辑,首先需要通过get方法根据传入path数组中的key之前的元素获取父级模块,然后使用addChild方法添加模块,添加的模块名就是之前传入到path数组的key,这样就建立好了父子关系。

这里可能有点难以理解,请看下面的示例代码。

// 原始数据
modules: {
  moduleA: {/* */},
  moduleB: {/* */}
}
 
 
// 初步收集模块后的 _modules 属性
_modules: {
  root: {
    _children: {
      moduleA: {/* Module instance */},
      moduleB: {/* Module instance */}
    }
  }
}

下面看下它是怎么获取父级模块的。

get(path) {
  return path.reduce((module, key) => {
    return module.getChild(key)
  }, this.root)
}

在上面的示例代码中,模块对应的path数组中只有模块名一个元素,取它之前的元素构成一个新的path,此时path其实是一个空数组[],这时获取到的父级模块就是根模块。所以会把moduleAmoduleB这两个模块添加到根模块的_children属性中。

Module模块类#

模块的实例对象是通过Module生成的,它里面定义了一些操作模块的属性和方法。

查看Module类的代码,在module/module.js文件中。

export default class Module {
  constructor(rawModule, runtime) {
    this.runtime = runtime; // ! 存储 runtime 的值
    // Store some children item
    this._children = Object.create(null); // ! 存储子模块
    // Store the origin module object which passed by programmer
    this._rawModule = rawModule; // ! 存储原始模块数据
    const rawState = rawModule.state; // ! 获取根的 state 原始数据
 
    // Store the origin module's state
    this.state = (typeof rawState === "function" ? rawState() : rawState) || {}; // ! 存储 state
  }
}

在类的构造函数中,定义了一些属性,查看上面代码的注释。比如我们刚才用到的_children属性就是用来存储子模块的。

另外还定义了一些方法,比如我们刚才用到的添加子模块的方法。

// ! 增加子模块
addChild(key, module) {
  this._children[key] = module
}

就是把子模块添加到_children属性中,以键值对的形式存储。

到这里,模块的收集工作就完成了,其实就是通过用户输入的原始模块数据,创建一个根模块。这个根模块对应的key,就是root,即对象{ root: root Module},然后把这个对象添加到store实例的_modules属性中。

下面继续看Store类的构造函数剩下的代码。

// bind commit and dispatch to self
const store = this;
const { dispatch, commit } = this;
 
// ! 绑定 this,指向 store 实例本身
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);
};
// ...

这里绑定commitdispatch方法的this指向,它们的this都指向store实例本身。

为什么要这么绑定呢?因为我们把store实例对象赋值给 Vue 的$store属性中,所以我们在实例组件中就可以像下面这样调用commitdispatch方法。

this.$store.commit("xxx");
this.$store.dispatch("xxx");

此时,如果不绑定thiscommitdispatch方法中的this指向的就是调用的它们的 Vue 实例组件,而不是Store实例对象。但是 Vue 实例组件中并没有这两个方法的,这样就会出错。

继续看下面的代码。

// strict mode
// ! 在严格模式下,任何 mutation 处理函数以外修改 Vuex state 都会抛出错误。
this.strict = strict;
 
const state = this._modules.root.state; // ! 获取根的 state

设置严格模式和获取根的 state 数据。严格模式一般只在开发环境中启用,而不能再生成环境中启用,理由在后面会讲。

继续往下看代码。

// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root); // ! ② 安装 root 模块 ,模块初始化

调用installModule函数安装模块。这里是Store实例初始化的第二个重点阶段:模块安装。

模块安装#

安装模块,说白了就是初始化store实例的一些属性,在前面模块收集时只是初步处理了原始数据,这里会更进一步处理原始数据,把处理后的数据放到一开始定义的属性中,比如_actions_mutations等属性中。

查看installModule函数的代码,在store.js文件中。

function installModule(store, rootState, path, module, hot) {
  const isRoot = !path.length; // ! 判断是否是根模块
  const namespace = store._modules.getNamespace(path); // ! 获取命名空间模块的名称 'moduleName/'
  // ...
}

先看函数体前面的代码,首先判断是否是根模块,当path是一个空数组时,传入的模块module就是根模块。

然后获取有命名空间模块的名称,是通过ModuleCollection实例对象的getNamespace方法获取的,看下这个方法的代码。

// ! 获取命名空间模块的名称
getNamespace(path) {
  let module = this.root // ! 获取根模块
  return path.reduce((namespace, key) => {
    module = module.getChild(key) // ! 获取子模块
 
    // ! 子模块 key 设置了命名空间,获取 key,并且拼接 '/'
    // ! 第一轮循环:path = [ moduleName ],namespace = '',key = moduleName =>  'moduleName/'
    return namespace + (module.namespaced ? key + '/' : '')
  }, '')
}

这里通过path获取有命名空间的模块的名称。因为Module的实例对象中是通过_children属性存储子模块的,它的存储方式是键值对结构,其中的键名就是模块名,然后模块名在模块收集时,已经通过调用注册方法register把它放入到path数组中,所以可以通过不断的遍历,使用getChild方法获取子模块,然后在最后返回它的模块名。另外还需要在模块名后面拼接/符号,是为了在后面更好的拼接模块内的类型名。

继续看下面的代码。

// register in namespace map
// ! 如果设置了命名空间,即 namespaced = true
if (module.namespaced) {
  if (
    store._modulesNamespaceMap[namespace] &&
    process.env.NODE_ENV !== "production"
  ) {
    console.error(
      `[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join(
        "/"
      )}`
    );
  }
  store._modulesNamespaceMap[namespace] = module; // ! 赋值到命名映射表中
}
 
// ...

有了模块名和模块,就把它们赋值到_modulesNamespaceMap属性中,方面后面使用。在注册之前还需要判断_modulesNamespaceMap映射表中是否已经存储这个模块,如果存在,在开发环境中会报错,提示你模块重复,这时候需要检测下是否编写了相同的模块。

成功赋值后,_modulesNamespaceMap映射表属性的示例代码就会像下面这样。

{
  moduleA/: {/* Module instance */},
  moduleB/: {/* Module instance */}
}

继续看下面的代码。

// set state
if (!isRoot && !hot) {
  const parentState = getNestedState(rootState, path.slice(0, -1)); // ! 获取父级的 state
  const moduleName = path[path.length - 1]; // ! 获取模块名
  store._withCommit(() => {
    Vue.set(parentState, moduleName, module.state); // ! 设置子模块,建立父子关系,并且为响应性数据
  });
}

这里是设置state数据,把所有模块下面的state数据统一放入到 root 的state属性中。

这里通过getNestedState函数来获取模块下面的state,通过path数组来查找。

function getNestedState(state, path) {
  return path.length ? path.reduce((state, key) => state[key], state) : state;
}

比如根的state数据和模块的state的结构之前是这样的。

// root
state: {
  count: 0;
}
 
// moduleA
state: {
  count: 1;
}
 
// moduleB
state: {
  count: 2;
}

把所有模块的state数据放到 root 的state中就变成下面这样子。后面还会把state数据放入到 Vue 组件的data属性下面,变成响应式数据,这里先买个关子。

// root state
state: {
  count: 0
  moduleA: {
    count: 1
  },
  moduleB: {
    count: 2
  }
}

继续看下面的代码。

// ! 构造了一个本地上下文环境(模块内部)
// ! local 中的 commit dispatch state getter 的效果会不一样
const local = (module.context = makeLocalContext(store, namespace, path));

这里使用makeLocalContext函数创建一个模块上下文环境(module.context属性),重写local下面的一些属性。

makeLocalContext创建模块上下文#

查看makeLocalContext函数的代码,在store.js文件中。

function makeLocalContext(store, namespace, path) {
  const noNamespace = namespace === ''
 
  // ! 创建 local 属性
  // ! 重写有命名空间的模块里面的 dispatch commit 方法和 getters state 属性
  const local = {
    dispatch: noNamespace
      ? store.dispatch // ! 没有命名空间,直接使用根的 dispatch --> dispatch(actionName, payload)
      : // ... 省略重写的方法
 
    commit: noNamespace
      ? store.commit
      : // ... 省略重写的方法
  }
 
  // 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) // ! 使用一个对象来代理模块里面的 getters
    },
    state: {
      get: () => getNestedState(store.state, path) // ! 通过路径获取嵌套的 state
    }
  })
 
  return local
}

先声明常量noNamespace表示是否存在命名空间的模块,它的值是true时表示没有,即namespace为空字符串。

然后定义了一个local常量,里面重写了在有命名空间中的模块里面的dispatchcommitgettersstate的逻辑,最后返回local这个常量。

先看下重写的dispatch函数。

dispatch: noNamespace
  ? store.dispatch // ! 没有命名空间模块,直接使用根的 dispatch --> dispatch(actionName, payload)
	: // ! 有命名空间,提交的类型 type 不一样
  (_type, _payload, _options) => {
    const args = unifyObjectStyle(_type, _payload, _options)
    const { payload, options } = args
    let { type } = args
 
    // ! options: { root: true } -> 也不会拼接命名空间
    if (!options || !options.root) {
      type = namespace + type // ! 拼接 type => 'moduleName/actionName'
      if (
        process.env.NODE_ENV !== 'production' &&
        !store._actions[type]
      ) {
        console.error(
          `[vuex] unknown local action type: ${args.type}, global type: ${type}`
        )
        return
      }
    }
 
   // ! 传入新的 type 为参数 --> dispatch(moduleName/actionName, payload)
   return store.dispatch(type, payload)
},

首先还是判断noNamespace的值,如果没有命名空间的模块,就直接调用store.dispatch,它的参数中的type不变,就是我们传入的值。

dispatch(actionName, payload); // ! 这里的 actionName 是 root 下面的

如果有命名空间的模块或者没有设置第三个选项参数{root: true},会拼接type,使得type变成moduleName/actionName,也就是说在模块中提交action时,提交的actionName会变成moduleName/actionName,这样就和 root 中的actionName很好的区分开来,调用的时候就是调用moduleName/actionName函数,而不是actionName函数。

重写的commit方法和dispatch逻辑一样,这里就不多赘述。

下面看下重写gettersstate代码,它们有点不一样。

Object.defineProperties(local, {
  getters: {
    get: noNamespace
      ? () => store.getters
      : () => makeLocalGetters(store, namespace), // ! 使用一个对象来代理模块里面的 getters
  },
  state: {
    get: () => getNestedState(store.state, path), // ! 通过路径获取嵌套的 state
  },
});

先看getters,如果没有命名空间模块,函数的返回值是store.getters,否则函数返回值是_makeLocalGettersCache属性中对应的值,它是一个代理对象,这个代理对象是通过makeLocalGetters函数生成的。

看下makeLocalGetters函数的代码。

function makeLocalGetters(store, namespace) {
  // ! 添加模块的 getters 到 _makeLocalGettersCache 中
  if (!store._makeLocalGettersCache[namespace]) {
    const gettersProxy = {};
    const splitPos = namespace.length; // ! 分割点:namespace 的长度
    Object.keys(store.getters).forEach((type) => {
      // skip if the target getter is not match this namespace
      if (type.slice(0, splitPos) !== namespace) return; // ! 命名空间和 type 的模块名不一致时直接返回,即没有匹配成功
 
      // extract local getter type
      const localType = type.slice(splitPos); // ! 截取 type 名称:moduleName/getterName --> getterName
 
      // Add a port to the getters proxy.
      // Define as getter property because
      // we do not want to evaluate the getters in this time.
      // ! 代理 gettersProxy,gettersProxy.localType === store.getters[type]
      Object.defineProperty(gettersProxy, localType, {
        get: () => store.getters[type],
        enumerable: true,
      });
    });
    store._makeLocalGettersCache[namespace] = gettersProxy;
  }
 
  return store._makeLocalGettersCache[namespace];
}

首先需要判断_makeLocalGettersCache属性中是否有模块,如果没有需要把模块的getters添加进去。声明常量gettersProxy为一个空对象,用来代理模块的getters,最后返回这个对象。声明常量splitPos存储namespace的长度值。

然后遍历store.getterskeys,当命名空间的值和type的模块名不一样时直接返回,即没有匹配到相应的模块。如果匹配成功,声明常量localType截取 type 的名称,即原来是moduleName/getterName的名称现在变成getterName。然后使用Object.defineProperty定义gettersProxy对象的localType属性,但是它的getter返回的确实store.getters中的type的值。

这里请注意下,在store.getters中的type键名是带模块名前缀的,而localType键名是不带模块名前缀的。

这里也有点绕,其实就是在模块里面访问getter属性是不需要带模块名前缀的,直接访问。但是我们在输出它的值时从store.getters属性中获取的,这里属性是带有模块名前缀的,不然输出的就不是模块的getter,而是 root 的getter。我们现在还不知道store.getters属性的结构,后面知道后就会比较清楚了。

最后看下state,它是直接通过getNestedState来获取state的值,不管是 root 的state值还是在模块的state值。因为在定义模块上下文之前,我们已经把所有模块的state已经被放在 root 的state中,所以通过getNestedState函数可以很好获取它们的值。

创建好上下文对象local之后,我们看下installModule剩下的代码。

// ! 遍历和注册模块,下同
module.forEachMutation((mutation, key) => {
  const namespacedType = namespace + key; // ! 拼接 type -> 'moduleName/mutationName'
  registerMutation(store, namespacedType, mutation, local);
});
 
module.forEachAction((action, key) => {
  const type = action.root ? key : namespace + key; // ! 拼接 type -> 'moduleName/actionName'
  const handler = action.handler || action; // ! 获取 action 函数
  registerAction(store, type, handler, local);
});
 
module.forEachGetter((getter, key) => {
  const namespacedType = namespace + key; // ! 拼接 type -> 'moduleName/getterName'
  registerGetter(store, namespacedType, getter, local);
});
 
// ! 递归注册子模块
module.forEachChild((child, key) => {
  installModule(store, rootState, path.concat(key), child, hot); // ! path 连接 key(模块名)
});

遍历模块中的存储的原始数据,然后注册这些原始数据。这里分别注册了mutationactiongetter和子模块。

Module类的构造函数中,我们知道原始的数据rawModule是存储在_rawModule属性中的。

先看下如何遍历mutations的值,查看forEachMutation方法的代码,在module/module.js文件中。

// ! 遍历并处理 mutations
forEachMutation(fn) {
  if (this._rawModule.mutations) {
    forEachValue(this._rawModule.mutations, fn)
  }
}

在看下工具函数forEachValue的代码,在util.js文件中

/**
 * forEach for object
 * ! 使用函数处理对象的所有 value 和 key
 */
export function forEachValue(obj, fn) {
  Object.keys(obj).forEach((key) => fn(obj[key], key));
}

这个函数遍历传入的对象objkeys,然后在调用传入的函数fn处理对象的valuekey

forEachMutation函数就是遍历传入的mutations对象,然后处理对象里面的valuekey

module.forEachMutation((mutation, key) => {
  const namespacedType = namespace + key; // ! 拼接 type -> 'moduleName/mutationName'
  registerMutation(store, namespacedType, mutation, local);
});

函数体中先声明常量namespacedType存储拼接namespacekey后的值,然后调用函数registerMutation注册mutation

看下registerMutation函数的代码,它是如何注册mutation的。

function registerMutation(store, type, handler, local) {
  // ! { 'moduleName/mutationName': entry }
  const entry = store._mutations[type] || (store._mutations[type] = []);
 
  // ! _mutations = { 'moduleName/mutationName': [wrappedMutationHandler] }
  entry.push(function wrappedMutationHandler(payload) {
    handler.call(store, local.state, payload); // ! mutationFn(local.state, payload) -> 第一个参数是模块的 state
  });
}

其实就是把原始的mutations放入到store实例属性的_mutations中。注意这里就用到了模块上下文local,在第一个参数中传入的是local.state,即模块的state,这里的state就是通过getNestedState函数获取的,不管是 root 的state,还是模块的state,都能正确获取到值。

其实在创建模块上下文的函数中,会先判断是否有命名空间模块,如果没有也会兼容没有命名空间模块的处理。所以看到使用了local的属性时,不要慌,他已经做了兼容处理,如果没有命名空间的模块的话,会按照 root 的值去处理。

接下来看下actions的遍历和注册。

// ! 注册 mutations,把 mutation 放入到 _mutations 中,并重写里面的函数
module.forEachAction((action, key) => {
  const type = action.root ? key : namespace + key; // ! 拼接 type -> 'moduleName/actionName'
  const handler = action.handler || action; // ! 获取 action 函数
  registerAction(store, type, handler, local);
});

mutation的逻辑差不多,不过从代码中可以看出action更加灵活,即可以是一个函数,也可以是一个对象。如果它是一个对象,会把函数放在对象的handler属性中。

看下registerAction的代码。

// ! 注册 actions,把 action 放入到 _actions 中,并重写里面的函数
function registerAction(store, type, handler, local) {
  const entry = store._actions[type] || (store._actions[type] = []);
  entry.push(function wrappedActionHandler(payload, cb) {
    let res = handler.call(
      store,
      // ! 第一个参数有很多选项,注意区分是模块 local 的属性还是根 store 的属性
      {
        dispatch: local.dispatch,
        commit: local.commit,
        getters: local.getters,
        state: local.state,
        rootGetters: store.getters,
        rootState: store.state,
      },
      payload,
      cb
    ); // ! actionFn({ commit... rootState }, payload, cb)
 
    // ! 判断返回值是否是 Promise,不是就调用 Promise.resolve() 转换成 Promise
    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;
    }
  });
}

逻辑也和mutation的组成差不多,只是action函数的第一个参数有非常多的选项。有四个模块的属性和两个 root 的属性。

另外,我们都知道action函数是可以进行异步执行的,所以这里会判断函数的返回值是不是一个 Promise 对象,如果不是的话,会使用Promise.resolve()方法把它转换成一个 Promise 对象。

下面看下getters的遍历和注册,逻辑和前面的一样,这里不再赘述了。

module.forEachGetter((getter, key) => {
  const namespacedType = namespace + key; // ! 拼接 type --> 'moduleName/getterName'
  registerGetter(store, namespacedType, getter, local);
});

再查看registerGetter函数的代码

// ! 注册 getters,把 getter 放入到 _wrappedGetters 中,并重写里面的函数
function registerGetter(store, type, rawGetter, local) {
  // ! 已经在里面了,就不要注册了
  if (store._wrappedGetters[type]) {
    if (process.env.NODE_ENV !== "production") {
      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
    ); // ! getterFn(state, getter, rootState, rootGetter)
  };
}

先判断下_wrappedGetters是否已经存在getterName,如果存在,即它们的key值相同,这时在开发环境中会报错。这说明可能创建了两个一样的模块,需要删除其中的一个模块。

注意这里的是_wrappedGetters属性,而不是getters属性,_wrappedGetters里面的函数包装了原始的rawGetter,然后把包装后的wrappedGetter函数组成的对象赋值给_wrappedGetters属性。

看下installModule函数的最后代码,递归注册子模块。

// ! 递归注册子模块
module.forEachChild((child, key) => {
  installModule(store, rootState, path.concat(key), child, hot); // ! path 连接 key(模块名)
});

先看下模块的forEachChild方法

// ! 遍历并处理子模块
forEachChild(fn) {
  forEachValue(this._children, fn)
}

这里遍历的是模块的子模块this._children的值。然后递归调用installModule进行注册,注意path的变化,这里不在是空数组,而是合并了key值(即子模块的名称)。

模块安装完成后,继续看Store的构造函数剩下的代码。

// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state); // ! ③ 初始化 store._vm

调用resetStoreVM函数初始化store._vm属性。这里是Store实例初始化的第三个重点阶段:初始化store._vm

初始化store._vm#

这里传入的参数是store实例自身和 root 的state的值。

下面看下resetStoreVM函数的代码,在store.js文件中。

function resetStoreVM(store, state, hot) {
  const oldVm = store._vm; // ! 缓存旧的 VM,用于热重载
 
  // bind store public getters
  store.getters = {}; // ! 创建 getters 属性
  const wrappedGetters = store._wrappedGetters; // ! 获取 wrappedGetters 对象
  const computed = {}; // ! 设置计算属性对象
  // ...
}

首先缓存旧的store._vm用于热重载时销毁,然后定义store.getters的初始值为一个空对象,注意这里才正式开始定于getters属性,再声明常量wrappedGetters存储store._wrappedGetters,声明computed对象,初始值是一个空对象。

继续看下面的代码。

// ! 遍历 wrappedGetters
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
  // ! 把 wrappedGetter 函数的返回值赋值到 VM 的 computed 中
  computed[key] = partial(fn, store); // ! fn(store) -> wrappedGetter(store)
  // ! 定义 store.getters 的属性
  // ! store.getters.xxx -> store._vm[xxx] -> store._vm.computed[xxx]
  Object.defineProperty(store.getters, key, {
    get: () => store._vm[key],
    enumerable: true, // for local getters
  });
});

这里遍历wrappedGetters,然后把它里面的数据赋值给上面声明的computed对象。注意这里是调用了fn函数,去掉函数包装,生成真正的getter

store.getters的数据结构如下:

getters: {
  getterFn(){}, // root getters
  'modulesA/getterFn'() {},
  'modulesA/getterFn2'() {},
  'modulesB/getterFn'() {}
}

然后使用Object.defineProperty定义key的属性中的getter的返回值为store._vm[key]

我们知道在 Vue 的实例对象中计算属性computed的值会出现在代理的实例对象中。后面会把computed对象作为计算属性注入到 Vue 的实例中,因为计算属性都会被代理到 Vue 的实例中,所以可以通过store._vm[key]获取到计算属性。当访问store.getters的时候,就是访问store._vm[key],也就是访问计算属性。

继续看下面的代码。

const silent = Vue.config.silent; // ! 缓存原来的 silent
Vue.config.silent = true; // ! 设为 true,将不会报任何警告
 
// ! 创建 store._vm 实例
// ! 绑定 state 和 getter 为 Vue 实例的 data 和 computed 属性,变成响应式数据
store._vm = new Vue({
  data: {
    $$state: state, // !  store.state -> store._vm.data.$$state
  },
  computed, // ! store._vm.computed[xxx] -> store._vm[xxx]
});
Vue.config.silent = silent; // ! 恢复原来的 silent

先处理 silent 的设置,然后生成一个新的 Vue 实例对象,赋值给store._vm,在生成的过程中把根数据state赋值为到 Vue 实例的data属性的$$state,然后把上面定义的computed对象也传入进去。这样storestategetters属性都变成了响应式数据。

我们都知道,Vuex 状态管理器的数据不同意一般的模块文件中的数据,它是响应式的,当我们修改了 Vuex 的数据时,依赖 Vuex 的数据的组件的视图会自动更新。这里的源码就是 Vuex 的数据变成响应式数据的原因。

继续看下面的代码。

// enable strict mode for new vm
// ! 在严格模式下,确保只能通过 commit 来显示的修改 state 的值
if (store.strict) {
  enableStrictMode(store);
}

在严格模式下,我们修改 Vuex 的数据必须显示的提交commit来进行修改,否则会报错。

看下enableStrictMode函数的代码。

// ! 执行严格模式
function enableStrictMode(store) {
  // ! 在开发环境,如果没有使用 commit 修改了 state 的值,会报错
  store._vm.$watch(
    function () {
      return this._data.$$state; // ! 严格模式下监听 store.state 值的变化
    },
    () => {
      if (process.env.NODE_ENV !== "production") {
        assert(
          store._committing,
          `do not mutate vuex store state outside mutation handlers.`
        );
      }
    },
    { deep: true, sync: true } // ! 深度监听和同步执行,有性能消耗,只能在开发环境使用 strict
  );
}

这里通过 Vue 实例的侦听器$watch去监听state的变化,在变化的时候,如果store._committingfalse会在开发环境中报错。同时还设置了深度监听和同步执行,这样会有很大的性能消耗,所以strick只能在开发环境中开启,千万不要在生产环境时开启。

继续看下面的代码。

// ! 热重载处理
if (oldVm) {
  if (hot) {
    // dispatch changes in all subscribed watchers
    // to force getter re-evaluation for hot reloading.
    store._withCommit(() => {
      oldVm._data.$$state = null; // ! 数据重置为 null
    });
  }
  Vue.nextTick(() => oldVm.$destroy()); // ! 销毁旧的 VM
}

这里是 Vuex 热重载的设置,热重载后需要重置旧的 VM 的数据和销毁旧的 VM 实例。

初始化store._vm完成后,继续看Store类的构造函数剩下的最后代码。

// apply plugins
plugins.forEach((plugin) => plugin(this)); // ! 调用所有插件
 
// ! 处理 devtool 插件
const useDevtools =
  options.devtools !== undefined ? options.devtools : Vue.config.devtools;
if (useDevtools) {
  devtoolPlugin(this); // ! 安装 devtool 插件
}

调用所有的插件,最后设置官方的 devtool 插件。

到了这一步,Store的实例对象的初始化就完成了,其实这也就是 Vuex 的初始化。在我们使用script标签使用 Vuex 的时候,此时 Vue 库的代码必须先引入,然后再引入 Vuex 库的代码,这样才能使用 Vuex。在用外链引入 Vue 和 Vue 的代码时, Vue 的构造函数会被赋值给window.Vue且浏览器对象window也是有定义的,store.js文件中的全局变量Vue也还没有赋值,此时才会安装 Vuex。

示例代码如下。

<body>
  <div id="app"></div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.js"></script>
  <script src="https://cdn.bootcss.com/vuex/3.1.1/vuex.js"></script>
  <script>
    var store = new Vuex.Store({
      state: {
        count: 0,
      },
      mutations: {
        addOne(state) {
          state.count += 1;
        },
      },
    });
 
    new Vue({
      template: `<div v-on:click="$store.commit('addOne')">count: {{$store.state.count}}</div>`,
      store,
    }).$mount("#app");
  </script>
</body>

继续看下面的代码。,是在开发环境中,执行三个断言。

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.`);
}

先看下断言函数assert的代码,在util.js文件中

// ! 断言:当条件没达成的时候,抛出错误
export function assert(condition, msg) {
  if (!condition) throw new Error(`[vuex] ${msg}`);
}

这个函数很简单,没有满足断言条件的话,就会抛出错误。

下面分析下三个断言:

  • Vue 必须有值(存在),因为 Vuex 只有依赖 Vue 才能使用。
  • 必须是支持 Promise 的环境,因为 Vuex 中的 actions 使用了 Promise 语法来处理异步。
  • 传入到 Vue 的选项中的store实例对象必须是通过操作符 new 出来的Store实例。

继续看下面的代码。

// ! 获取选项中的插件(默认是空数组)和严格模式定义(默认是 false)
const { plugins = [], strict = false } = options;
 
// store internal state
this._committing = false; // ! 判断是否使用 commit 修改数据
this._actions = Object.create(null); // ! 存储 actions
this._actionSubscribers = []; // ! 存储 action 的所有订阅函数
this._mutations = Object.create(null); // ! 存储 mutations
this._wrappedGetters = Object.create(null); // ! 存储 wrapper getters
this._modules = new ModuleCollection(options); // ! ① 模块收集 => { root: rootModule }
this._modulesNamespaceMap = Object.create(null); // ! 模块命名映射表 { 'moduleName/': module}
this._subscribers = []; // ! 存储 mutation 的所有订阅函数
this._watcherVM = new Vue(); // ! 创建一个 Vue 实例,用来使用实例属性 $watch 实现 watch API
this._makeLocalGettersCache = Object.create(null); // ! 模块的 getters
 
// ...

options获取插件和strict模式。

然后定义了一堆的属性,属性解析请看上面的注释。

这里主要看下this._modules属性,它是ModuleCollection类的实例对象,通过创建这个实例对象,进行模块的收集。

this._modules = new ModuleCollection(options); // ! ① 模块收集 => { root: rootModule }

这里是Store实例初始化的第一个重点阶段:模块收集。

模块收集#

ModuleCollection模块收集类#

查看ModuleCollection类的代码,在module/module-collection.js文件中。

// ! 模块收集类,设置 root 模块
export default class ModuleCollection {
  constructor(rawRootModule) {
    // register root module (Vuex.Store options)
    this.register([], rawRootModule, false); // ! 初始化时注册模块
  }
 
  // ...
}

这个类的主要作用是收集模块,它会创建一个{ root: Module instance}实例对象。

看类的构造函数,在初始化时只调用register方法来注册模块。

查看register方法的代码。

register(path, rawModule, runtime = true) {
  // ! 开发模式下断言原始数据,判断输入的数据类型和格式是否有错
  if (process.env.NODE_ENV !== 'production') {
    assertRawModule(path, rawModule)
  }
 
  const newModule = new Module(rawModule, runtime) // ! 创建一个模块
 
  // ! path 为空时
  if (path.length === 0) {
    this.root = newModule // ! 创建的模块为根模块,注意:this.root 是这里类唯一的属性值
  } else {
    const parent = this.get(path.slice(0, -1)) // ! 根据路径获取到父模块(在数组里面它前面的元素)
    parent.addChild(path[path.length - 1], newModule) // ! 添加子模块,建立父子关系
  }
 
  // register nested modules
  // ! 用户自定义模块,注册嵌套模块
  if (rawModule.modules) {
    forEachValue(rawModule.modules, (rawChildModule, key) => {
      // ! 把 key 放入到 path 中,key === moduleName
      this.register(path.concat(key), rawChildModule, runtime)
    })
  }
}

首先在开发环境中会使用断言函数校验用户输入的原始选项数据的类型或者格式是否有错。

然后声明常量newModule,存储通过操作符 new 创建一个Module实例对象的。先不管生成的实例对象是怎么样子的,继续看下面的代码。。

如果path数组的长度为 0 时,生成的模块被赋值给this.root,即生成根模块。注意:root属性是类ModuleCollection实例对象的唯一一个的属性值。我们在初始化模块时,path是一个空数组,所有先不看 else 代码块的代码,后面再解析。

继续看下面的代码。,如果我们的原始数据中设置了模块,会遍历这些模块,然后还是通过register方法注册这些模块。这里注册时,path就不是一个空数组了,而是合并了key,也就是模块名。这时,会进入上面的 else 代码块的逻辑,首先需要通过get方法根据传入path数组中的key之前的元素获取父级模块,然后使用addChild方法添加模块,添加的模块名就是之前传入到path数组的key,这样就建立好了父子关系。

这里可能有点难以理解,请看下面的示例代码。

// 原始数据
modules: {
  moduleA: {/* */},
  moduleB: {/* */}
}
 
 
// 初步收集模块后的 _modules 属性
_modules: {
  root: {
    _children: {
      moduleA: {/* Module instance */},
      moduleB: {/* Module instance */}
    }
  }
}

下面看下它是怎么获取父级模块的。。

get(path) {
  return path.reduce((module, key) => {
    return module.getChild(key)
  }, this.root)
}

在上面的示例代码中,模块对应的path数组中只有模块名一个元素,取它之前的元素构成一个新的path,此时path其实是一个空数组[],这时获取到的父级模块就是根模块。所以会把moduleAmoduleB这两个模块添加到根模块的_children属性中。

Module模块类#

模块的实例对象是通过Module生成的,它里面定义了一些操作模块的属性和方法。

查看Module类的代码,在module/module.js文件中。

export default class Module {
  constructor(rawModule, runtime) {
    this.runtime = runtime; // ! 存储 runtime 的值
    // Store some children item
    this._children = Object.create(null); // ! 存储子模块
    // Store the origin module object which passed by programmer
    this._rawModule = rawModule; // ! 存储原始模块数据
    const rawState = rawModule.state; // ! 获取根的 state 原始数据
 
    // Store the origin module's state
    this.state = (typeof rawState === "function" ? rawState() : rawState) || {}; // ! 存储 state
  }
}

在类的构造函数中,定义了一些属性,查看上面代码的注释。比如我们刚才用到的_children属性就是用来存储子模块的。

另外还定义了一些方法。比如我们刚才用到的添加子模块的方法。

// ! 增加子模块
addChild(key, module) {
  this._children[key] = module
}

就是把子模块添加到_children属性中,以键值对的形式存储。

到这里,模块的收集工作就完成了,其实就是通过用户输入的原始模块数据,创建一个根模块。这个根模块对应的key,就是root,即对象{ root: root Module},然后把这个对象添加到store实例的_modules属性中。

下面继续看Store类的构造函数剩下的代码。

// bind commit and dispatch to self
const store = this;
const { dispatch, commit } = this;
 
// ! 绑定 this,指向 store 实例本身
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);
};
// ...

这里绑定commitdispatch方法的this指向,它们的this都指向store实例本身。

为什么要这么绑定呢?因为我们把store实例对象赋值给 Vue 的$store属性中,所以我们在实例组件中就可以像下面这样调用commitdispatch方法。

this.$store.commit("xxx");
this.$store.dispatch("xxx");

此时,如果不绑定thiscommitdispatch方法中的this指向的就是调用的它们的 Vue 实例组件,而不是Store实例对象。但是 Vue 实例组件中并没有这两个方法的,这样就会出错。

继续看下面的代码。

// strict mode
// ! 在严格模式下,任何 mutation 处理函数以外修改 Vuex state 都会抛出错误。
this.strict = strict;
 
const state = this._modules.root.state; // ! 获取根的 state

设置严格模式和获取根的 state 数据。严格模式一般只在开发环境中启用,而不能再生成环境中启用,理由在后面会讲。

继续往下看代码。

// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root); // ! ② 安装 root 模块 ,模块初始化

调用installModule函数安装模块。这里是Store实例初始化的第二个重点阶段:模块安装。

模块安装#

安装模块,说白了就是初始化store实例的一些属性,在前面模块收集时只是初步处理了原始数据,这里会更进一步处理原始数据,把处理后的数据放到一开始定义的属性中,比如_actions_mutations等属性中。

查看installModule函数的代码,在store.js文件中。

function installModule(store, rootState, path, module, hot) {
  const isRoot = !path.length; // ! 判断是否是根模块
  const namespace = store._modules.getNamespace(path); // ! 获取命名空间模块的名称 'moduleName/'
  // ...
}

先看函数体前面的代码,首先判断是否是根模块,当path是一个空数组时,传入的模块module就是根模块。

然后获取有命名空间模块的名称,是通过ModuleCollection实例对象的getNamespace方法获取的,看下这个方法的代码。

// ! 获取命名空间模块的名称
getNamespace(path) {
  let module = this.root // ! 获取根模块
  return path.reduce((namespace, key) => {
    module = module.getChild(key) // ! 获取子模块
 
    // ! 子模块 key 设置了命名空间,获取 key,并且拼接 '/'
    // ! 第一轮循环:path = [ moduleName ],namespace = '',key = moduleName =>  'moduleName/'
    return namespace + (module.namespaced ? key + '/' : '')
  }, '')
}

这里通过path获取有命名空间的模块的名称。因为Module的实例对象中是通过_children属性存储子模块的,它的存储方式是键值对结构,其中的键名就是模块名,然后模块名在模块收集时,已经通过调用注册方法register把它放入到path数组中,所以可以通过不断的遍历,使用getChild方法获取子模块,然后在最后返回它的模块名。另外还需要在模块名后面拼接/符号,是为了在后面更好的拼接模块内的类型名。

继续看下面的代码。

// register in namespace map
// ! 如果设置了命名空间,即 namespaced = true
if (module.namespaced) {
  if (
    store._modulesNamespaceMap[namespace] &&
    process.env.NODE_ENV !== "production"
  ) {
    console.error(
      `[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join(
        "/"
      )}`
    );
  }
  store._modulesNamespaceMap[namespace] = module; // ! 赋值到命名映射表中
}
 
// ...

有了模块名和模块,就把它们赋值到_modulesNamespaceMap属性中,方面后面使用。在注册之前还需要判断_modulesNamespaceMap映射表中是否已经存储这个模块,如果存在,在开发环境中会报错,提示你模块重复,这时候需要检测下是否编写了相同的模块。

成功赋值后,_modulesNamespaceMap映射表属性的示例代码就会像下面这样。

{
  moduleA/: {/* Module instance */},
  moduleB/: {/* Module instance */}
}

继续看下面的代码。

// set state
if (!isRoot && !hot) {
  const parentState = getNestedState(rootState, path.slice(0, -1)); // ! 获取父级的 state
  const moduleName = path[path.length - 1]; // ! 获取模块名
  store._withCommit(() => {
    Vue.set(parentState, moduleName, module.state); // ! 设置子模块,建立父子关系,并且为响应性数据
  });
}

这里是设置state数据,把所有模块下面的state数据统一放入到 root 的state属性中。

这里通过getNestedState函数来获取模块下面的state,通过path数组来查找。

function getNestedState(state, path) {
  return path.length ? path.reduce((state, key) => state[key], state) : state;
}

比如根的state数据和模块的state的结构之前是这样的。

// root
state: {
  count: 0;
}
 
// moduleA
state: {
  count: 1;
}
 
// moduleB
state: {
  count: 2;
}

把所有模块的state数据放到 root 的state中就变成下面这样子。后面还会把state数据放入到 Vue 组件的data属性下面,变成响应式数据,这里先买个关子。

// root state
state: {
  count: 0
  moduleA: {
    count: 1
  },
  moduleB: {
    count: 2
  }
}

继续看下面的代码。

// ! 构造了一个本地上下文环境(模块内部)
// ! local 中的 commit dispatch state getter 的效果会不一样
const local = (module.context = makeLocalContext(store, namespace, path));

这里使用makeLocalContext函数创建一个模块上下文环境(module.context属性),重写local下面的一些属性。

makeLocalContext创建模块上下文#

查看makeLocalContext函数的代码,在store.js文件中。

function makeLocalContext(store, namespace, path) {
  const noNamespace = namespace === ''
 
  // ! 创建 local 属性
  // ! 重写有命名空间的模块里面的 dispatch commit 方法和 getters state 属性
  const local = {
    dispatch: noNamespace
      ? store.dispatch // ! 没有命名空间,直接使用根的 dispatch --> dispatch(actionName, payload)
      : // ... 省略重写的方法
 
    commit: noNamespace
      ? store.commit
      : // ... 省略重写的方法
  }
 
  // 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) // ! 使用一个对象来代理模块里面的 getters
    },
    state: {
      get: () => getNestedState(store.state, path) // ! 通过路径获取嵌套的 state
    }
  })
 
  return local
}

先声明常量noNamespace表示是否存在命名空间的模块,它的值是true时表示没有,即namespace为空字符串。

然后定义了一个local常量,里面重写了在有命名空间中的模块里面的dispatchcommitgettersstate的逻辑,最后返回local这个常量。

先看下重写的dispatch函数

dispatch: noNamespace
  ? store.dispatch // ! 没有命名空间模块,直接使用根的 dispatch --> dispatch(actionName, payload)
	: // ! 有命名空间,提交的类型 type 不一样
  (_type, _payload, _options) => {
    const args = unifyObjectStyle(_type, _payload, _options)
    const { payload, options } = args
    let { type } = args
 
    // ! options: { root: true } -> 也不会拼接命名空间
    if (!options || !options.root) {
      type = namespace + type // ! 拼接 type => 'moduleName/actionName'
      if (
        process.env.NODE_ENV !== 'production' &&
        !store._actions[type]
      ) {
        console.error(
          `[vuex] unknown local action type: ${args.type}, global type: ${type}`
        )
        return
      }
    }
 
   // ! 传入新的 type 为参数 --> dispatch(moduleName/actionName, payload)
   return store.dispatch(type, payload)
},

首先还是判断noNamespace的值,如果没有命名空间的模块,就直接调用store.dispatch,它的参数中的type不变,就是我们传入的值。

dispatch(actionName, payload); // ! 这里的 actionName 是 root 下面的

如果有命名空间的模块或者没有设置第三个选项参数{root: true},会拼接type,使得type变成moduleName/actionName,也就是说在模块中提交action时,提交的actionName会变成moduleName/actionName,这样就和 root 中的actionName很好的区分开来,调用的时候就是调用moduleName/actionName函数,而不是actionName函数。

重写的commit方法和dispatch逻辑一样,这里就不多赘述。

下面看下重写gettersstate代码,它们有点不一样

Object.defineProperties(local, {
  getters: {
    get: noNamespace
      ? () => store.getters
      : () => makeLocalGetters(store, namespace), // ! 使用一个对象来代理模块里面的 getters
  },
  state: {
    get: () => getNestedState(store.state, path), // ! 通过路径获取嵌套的 state
  },
});

先看getters,如果没有命名空间模块,函数的返回值是store.getters,否则函数返回值是_makeLocalGettersCache属性中对应的值,它是一个代理对象,这个代理对象是通过makeLocalGetters函数生成的。

看下makeLocalGetters函数的代码

function makeLocalGetters(store, namespace) {
  // ! 添加模块的 getters 到 _makeLocalGettersCache 中
  if (!store._makeLocalGettersCache[namespace]) {
    const gettersProxy = {};
    const splitPos = namespace.length; // ! 分割点:namespace 的长度
    Object.keys(store.getters).forEach((type) => {
      // skip if the target getter is not match this namespace
      if (type.slice(0, splitPos) !== namespace) return; // ! 命名空间和 type 的模块名不一致时直接返回,即没有匹配成功
 
      // extract local getter type
      const localType = type.slice(splitPos); // ! 截取 type 名称:moduleName/getterName --> getterName
 
      // Add a port to the getters proxy.
      // Define as getter property because
      // we do not want to evaluate the getters in this time.
      // ! 代理 gettersProxy,gettersProxy.localType === store.getters[type]
      Object.defineProperty(gettersProxy, localType, {
        get: () => store.getters[type],
        enumerable: true,
      });
    });
    store._makeLocalGettersCache[namespace] = gettersProxy;
  }
 
  return store._makeLocalGettersCache[namespace];
}

首先需要判断_makeLocalGettersCache属性中是否有模块,如果没有需要把模块的getters添加进去。声明常量gettersProxy为一个空对象,用来代理模块的getters,最后返回这个对象。声明常量splitPos存储namespace的长度值。

然后遍历store.getterskeys,当命名空间的值和type的模块名不一样时直接返回,即没有匹配到相应的模块。如果匹配成功,声明常量localType截取 type 的名称,即原来是moduleName/getterName的名称现在变成getterName。然后使用Object.defineProperty定义gettersProxy对象的localType属性,但是它的getter返回的确实store.getters中的type的值。

这里请注意下,在store.getters中的type键名是带模块名前缀的,而localType键名是不带模块名前缀的。

这里也有点绕,其实就是在模块里面访问getter属性是不需要带模块名前缀的,直接访问。但是我们在输出它的值时从store.getters属性中获取的,这里属性是带有模块名前缀的,不然输出的就不是模块的getter,而是 root 的getter。我们现在还不知道store.getters属性的结构,后面知道后就会比较清楚了。

最后看下state,它是直接通过getNestedState来获取state的值,不管是 root 的state值还是在模块的state值。因为在定义模块上下文之前,我们已经把所有模块的state已经被放在 root 的state中,所以通过getNestedState函数可以很好获取它们的值。

创建好上下文对象local之后,我们看下installModule剩下的代码。

// ! 遍历和注册模块,下同
module.forEachMutation((mutation, key) => {
  const namespacedType = namespace + key; // ! 拼接 type -> 'moduleName/mutationName'
  registerMutation(store, namespacedType, mutation, local);
});
 
module.forEachAction((action, key) => {
  const type = action.root ? key : namespace + key; // ! 拼接 type -> 'moduleName/actionName'
  const handler = action.handler || action; // ! 获取 action 函数
  registerAction(store, type, handler, local);
});
 
module.forEachGetter((getter, key) => {
  const namespacedType = namespace + key; // ! 拼接 type -> 'moduleName/getterName'
  registerGetter(store, namespacedType, getter, local);
});
 
// ! 递归注册子模块
module.forEachChild((child, key) => {
  installModule(store, rootState, path.concat(key), child, hot); // ! path 连接 key(模块名)
});

遍历模块中的存储的原始数据,然后注册这些原始数据。这里分别注册了mutationactiongetter和子模块。

Module类的构造函数中,我们知道原始的数据rawModule是存储在_rawModule属性中的。

先看下如何遍历mutations的值,查看forEachMutation方法的代码,在module/module.js文件中

// ! 遍历并处理 mutations
forEachMutation(fn) {
  if (this._rawModule.mutations) {
    forEachValue(this._rawModule.mutations, fn)
  }
}

在看下工具函数forEachValue的代码,在util.js文件中。

/**
 * forEach for object
 * ! 使用函数处理对象的所有 value 和 key
 */
export function forEachValue(obj, fn) {
  Object.keys(obj).forEach((key) => fn(obj[key], key));
}

这个函数遍历传入的对象objkeys,然后在调用传入的函数fn处理对象的valuekey

forEachMutation函数就是遍历传入的mutations对象,然后处理对象里面的valuekey

module.forEachMutation((mutation, key) => {
  const namespacedType = namespace + key; // ! 拼接 type -> 'moduleName/mutationName'
  registerMutation(store, namespacedType, mutation, local);
});

函数体中先声明常量namespacedType存储拼接namespacekey后的值,然后调用函数registerMutation注册mutation

看下registerMutation函数的代码,它是如何注册mutation的。

function registerMutation(store, type, handler, local) {
  // ! { 'moduleName/mutationName': entry }
  const entry = store._mutations[type] || (store._mutations[type] = []);
 
  // ! _mutations = { 'moduleName/mutationName': [wrappedMutationHandler] }
  entry.push(function wrappedMutationHandler(payload) {
    handler.call(store, local.state, payload); // ! mutationFn(local.state, payload) -> 第一个参数是模块的 state
  });
}

其实就是把原始的mutations放入到store实例属性的_mutations中。注意这里就用到了模块上下文local,在第一个参数中传入的是local.state,即模块的state,这里的state就是通过getNestedState函数获取的,不管是 root 的state,还是模块的state,都能正确获取到值。

其实在创建模块上下文的函数中,会先判断是否有命名空间模块,如果没有也会兼容没有命名空间模块的处理。所以看到使用了local的属性时,不要慌,他已经做了兼容处理,如果没有命名空间的模块的话,会按照 root 的值去处理。

接下来看下actions的遍历和注册。

// ! 注册 mutations,把 mutation 放入到 _mutations 中,并重写里面的函数
module.forEachAction((action, key) => {
  const type = action.root ? key : namespace + key; // ! 拼接 type -> 'moduleName/actionName'
  const handler = action.handler || action; // ! 获取 action 函数
  registerAction(store, type, handler, local);
});

mutation的逻辑差不多,不过从代码中可以看出action更加灵活,即可以是一个函数,也可以是一个对象。如果它是一个对象,会把函数放在对象的handler属性中。

看下registerAction的代码。

// ! 注册 actions,把 action 放入到 _actions 中,并重写里面的函数
function registerAction(store, type, handler, local) {
  const entry = store._actions[type] || (store._actions[type] = []);
  entry.push(function wrappedActionHandler(payload, cb) {
    let res = handler.call(
      store,
      // ! 第一个参数有很多选项,注意区分是模块 local 的属性还是根 store 的属性
      {
        dispatch: local.dispatch,
        commit: local.commit,
        getters: local.getters,
        state: local.state,
        rootGetters: store.getters,
        rootState: store.state,
      },
      payload,
      cb
    ); // ! actionFn({ commit... rootState }, payload, cb)
 
    // ! 判断返回值是否是 Promise,不是就调用 Promise.resolve() 转换成 Promise
    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;
    }
  });
}

逻辑也和mutation的组成差不多,只是action函数的第一个参数有非常多的选项。有四个模块的属性和两个 root 的属性。

另外,我们都知道action函数是可以进行异步执行的,所以这里会判断函数的返回值是不是一个 Promise 对象,如果不是的话,会使用Promise.resolve()方法把它转换成一个 Promise 对象。

下面看下getters的遍历和注册,逻辑和前面的一样,这里不在赘述了。

module.forEachGetter((getter, key) => {
  const namespacedType = namespace + key; // ! 拼接 type --> 'moduleName/getterName'
  registerGetter(store, namespacedType, getter, local);
});

再查看registerGetter函数的代码

// ! 注册 getters,把 getter 放入到 _wrappedGetters 中,并重写里面的函数
function registerGetter(store, type, rawGetter, local) {
  // ! 已经在里面了,就不要注册了
  if (store._wrappedGetters[type]) {
    if (process.env.NODE_ENV !== "production") {
      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
    ); // ! getterFn(state, getter, rootState, rootGetter)
  };
}

先判断下_wrappedGetters是否已经存在getterName,如果存在,即它们的key值相同,这时在开发环境中会报错。这说明可能创建了两个一样的模块,需要删除其中的一个模块。

注意这里的是_wrappedGetters属性,而不是getters属性,_wrappedGetters里面的函数包装了原始的rawGetter,然后把包装后的wrappedGetter函数组成的对象赋值给_wrappedGetters属性。

看下installModule函数的最后代码,递归注册子模块。

// ! 递归注册子模块
module.forEachChild((child, key) => {
  installModule(store, rootState, path.concat(key), child, hot); // ! path 连接 key(模块名)
});

先看下模块的forEachChild方法。

// ! 遍历并处理子模块
forEachChild(fn) {
  forEachValue(this._children, fn)
}

这里遍历的是模块的子模块this._children的值。然后递归调用installModule进行注册,注意path的变化,这里不在是空数组,而是合并了key值(即子模块的名称)。

模块安装完成后,继续看Store的构造函数剩下的代码。

// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state); // ! ③ 初始化 store._vm

调用resetStoreVM函数初始化store._vm属性。这里是Store实例初始化的第三个重点阶段:初始化store._vm

初始化store._vm#

这里传入的参数是store实例自身和 root 的state的值。下面看下resetStoreVM函数的代码,在store.js文件中。

function resetStoreVM(store, state, hot) {
  const oldVm = store._vm; // ! 缓存旧的 VM,用于热重载
 
  // bind store public getters
  store.getters = {}; // ! 创建 getters 属性
  const wrappedGetters = store._wrappedGetters; // ! 获取 wrappedGetters 对象
  const computed = {}; // ! 设置计算属性对象
  // ...
}

首先缓存旧的store._vm用于热重载时销毁,然后定义store.getters的初始值为一个空对象,注意这里才正式开始定于getters属性,再声明常量wrappedGetters存储store._wrappedGetters,声明computed对象,初始值是一个空对象。

继续看下面的代码。

// ! 遍历 wrappedGetters
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
  // ! 把 wrappedGetter 函数的返回值赋值到 VM 的 computed 中
  computed[key] = partial(fn, store); // ! fn(store) -> wrappedGetter(store)
  // ! 定义 store.getters 的属性
  // ! store.getters.xxx -> store._vm[xxx] -> store._vm.computed[xxx]
  Object.defineProperty(store.getters, key, {
    get: () => store._vm[key],
    enumerable: true, // for local getters
  });
});

这里遍历wrappedGetters,然后把它里面的数据赋值给上面声明的computed对象。注意这里是调用了fn函数,去掉函数包装,生成真正的getter

store.getters的数据结构如下:

getters: {
  getterFn(){}, // root getters
  'modulesA/getterFn'() {},
  'modulesA/getterFn2'() {},
  'modulesB/getterFn'() {}
}

然后使用Object.defineProperty定义key的属性中的getter的返回值为store._vm[key]

我们知道在 Vue 的实例对象中计算属性computed的值会出现在代理的实例对象中。后面会把computed对象作为计算属性注入到 Vue 的实例中,因为计算属性都会被代理到 Vue 的实例中,所以可以通过store._vm[key]获取到计算属性。当访问store.getters的时候,就是访问store._vm[key],也就是访问计算属性。

继续看下面的代码。

const silent = Vue.config.silent; // ! 缓存原来的 silent
Vue.config.silent = true; // ! 设为 true,将不会报任何警告
 
// ! 创建 store._vm 实例
// ! 绑定 state 和 getter 为 Vue 实例的 data 和 computed 属性,变成响应式数据
store._vm = new Vue({
  data: {
    $$state: state, // !  store.state -> store._vm.data.$$state
  },
  computed, // ! store._vm.computed[xxx] -> store._vm[xxx]
});
Vue.config.silent = silent; // ! 恢复原来的 silent

先处理 silent 的设置,然后生成一个新的 Vue 实例对象,赋值给store._vm,在生成的过程中把根数据state赋值为到 Vue 实例的data属性的$$state,然后把上面定义的computed对象也传入进去。这样storestategetters属性都变成了响应式数据。

我们都知道,Vuex 状态管理器的数据不同意一般的模块文件中的数据,它是响应式的,当我们修改了 Vuex 的数据时,依赖 Vuex 的数据的组件的视图会自动更新。这里的源码就是 Vuex 的数据变成响应式数据的原因。

继续看下面的代码。

// enable strict mode for new vm
// ! 在严格模式下,确保只能通过 commit 来显示的修改 state 的值
if (store.strict) {
  enableStrictMode(store);
}

在严格模式下,我们修改 Vuex 的数据必须显示的提交commit来进行修改,否则会报错。

看下面enableStrictMode函数的代码。

// ! 执行严格模式
function enableStrictMode(store) {
  // ! 在开发环境,如果没有使用 commit 修改了 state 的值,会报错
  store._vm.$watch(
    function () {
      return this._data.$$state; // ! 严格模式下监听 store.state 值的变化
    },
    () => {
      if (process.env.NODE_ENV !== "production") {
        assert(
          store._committing,
          `do not mutate vuex store state outside mutation handlers.`
        );
      }
    },
    { deep: true, sync: true } // ! 深度监听和同步执行,有性能消耗,只能在开发环境使用 strict
  );
}

这里通过 Vue 实例的侦听器$watch去监听state的变化,在变化的时候,如果store._committingfalse会在开发环境中报错。同时还设置了深度监听和同步执行,这样会有很大的性能消耗,所以strick只能在开发环境中开启,千万不要在生产环境时开启。

继续看下面的代码。

// ! 热重载处理
if (oldVm) {
  if (hot) {
    // dispatch changes in all subscribed watchers
    // to force getter re-evaluation for hot reloading.
    store._withCommit(() => {
      oldVm._data.$$state = null; // ! 数据重置为 null
    });
  }
  Vue.nextTick(() => oldVm.$destroy()); // ! 销毁旧的 VM
}

这里是 Vuex 热重载的设置,热重载后需要重置旧的 VM 的数据和销毁旧的 VM 实例。

初始化store._vm完成后,继续看Store类的构造函数剩下的最后代码。

// apply plugins
plugins.forEach((plugin) => plugin(this)); // ! 调用所有插件
 
// ! 处理 devtool 插件
const useDevtools =
  options.devtools !== undefined ? options.devtools : Vue.config.devtools;
if (useDevtools) {
  devtoolPlugin(this); // ! 安装 devtool 插件
}

调用所有的插件,最后设置官方的 devtool 插件。

到了这一步,Store的实例对象的初始化就完成了,其实这也就是 Vuex 的初始化。

参考资料#