详细阐述 Vuex (Vue 3 版本或 Vue 2 版本) 源码中 `Store` 类的初始化过程,包括 `state`、`getters`、`mutations` 和 `actions` 的注册。

各位观众老爷,晚上好!今天咱们来聊聊 Vuex 的老底儿,扒一扒 Store 类的初始化过程,看看 Vuex 是怎么把 stategettersmutationsactions 这些宝贝疙瘩安排得井井有条的。 咱们尽量用大白话,加上点小幽默,保证大家听得懂、记得住。

开场白:Vuex 的本质是什么?

在深入源码之前,咱们先来明确一个概念:Vuex 其实就是一个状态管理容器,它把应用的状态集中管理起来,并且提供了一套规则来修改状态。可以把 Vuex 看作一个全局的“数据库”,专门用来存放和管理 Vue 应用的数据。

Store 类:Vuex 的心脏

Store 类是 Vuex 的核心,所有的状态管理逻辑都围绕着它展开。我们创建 Vuex 实例的时候,实际上就是在创建一个 Store 类的实例。

初始化过程:一步一个脚印

接下来,咱们就一步一个脚印,来剖析 Store 类的初始化过程。为了方便理解,咱们以 Vue 3 版本的 Vuex 为例,但 Vue 2 版本的思路也是大同小异,稍微改改就能用。

  1. 构造函数:Store 的诞生

首先,我们来看 Store 类的构造函数:

class Store {
  constructor(options = {}) {
    const {
      state = {},
      getters = {},
      mutations = {},
      actions = {},
      plugins = [],
      strict = false
    } = options;

    // 1. 严格模式
    this._committing = false;
    this.strict = strict;

    // 2. 获取 state
    this._state = typeof state === 'function' ? state() : state;

    // 3. 初始化 mutations
    this._mutations = Object.create(null);
    this._actions = Object.create(null);
    this._wrappedGetters = Object.create(null);

    // 4. 模块化
    this._modules = new ModuleCollection(options);

    // 5. 安装模块
    installModule(this, this.state, [], this._modules.root);

    // 6. 初始化 getters
    resetStoreVM(this, this._state);

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

    // 8. 订阅
    const devtools = getDevtoolsGlobalHook();
    devtools && devtools.emit('vuex:init', this);
    devtools && devtools.on('vuex:travel-to-state', state => {
      this.replaceState(state);
    });
  }
}

这个构造函数接收一个 options 对象,这个对象包含了 stategettersmutationsactions 等配置项。 让我们逐行解读一下:

  • strict: 严格模式开关。开启严格模式后,只有通过 mutation 才能修改 state,否则会抛出错误。这有助于我们在开发过程中避免意外的状态修改。

  • this._state: state 是 Vuex 存储数据的核心。如果 state 是一个函数,就调用它来获取初始状态;否则,直接使用传入的 state 对象。 这样做的好处是,如果 state 是一个函数,每次创建 Store 实例时,都会得到一个新的 state 对象,避免多个组件共享同一个 state 对象导致数据污染。

  • this._mutationsthis._actionsthis._wrappedGetters: 这三个对象分别用于存储 mutationsactionsgettersObject.create(null) 创建一个没有原型的空对象,避免原型链上的属性干扰。

  • this._modules: 用于处理模块化的情况。ModuleCollection 类负责将模块化的 options 对象转换成一个树形结构,方便后续的安装。

  • installModule: 这个函数是整个初始化过程的核心。它递归地安装模块,注册 mutationsactionsgetters

  • resetStoreVM: 这个函数负责创建 Vue 实例,并将 stategetters 挂载到 Vue 实例上。这样,我们就可以在组件中通过 this.$store.statethis.$store.getters 来访问状态和计算属性了。

  • plugins: Vuex 允许我们使用插件来扩展功能。plugins 数组中的每个插件都会被调用,并且传入 Store 实例作为参数。

  • devtools: 如果安装了 Vue Devtools,Vuex 会通过 devtools 对象与 Devtools 进行通信,方便我们调试 Vuex 应用。

  1. installModule:模块安装的关键

接下来,我们重点看一下 installModule 函数,它是注册 mutationsactionsgetters 的关键。

function installModule(store, rootState, path, module, hot) {
  const isRoot = !path.length;
  const namespaced = store._modules.getNamespace(path);

  // 1. 注册 mutation
  if (module.mutations) {
    Object.entries(module.mutations).forEach(([key, mutation]) => {
      const namespacedType = namespaced + key;
      registerMutation(store, namespacedType, mutation, module);
    });
  }

  // 2. 注册 action
  if (module.actions) {
    Object.entries(module.actions).forEach(([key, action]) => {
      const type = action.root ? key : namespaced + key;
      const handler = action.handler || action;
      registerAction(store, type, handler, module);
    });
  }

  // 3. 注册 getter
  if (module.getters) {
    Object.entries(module.getters).forEach(([key, getter]) => {
      const namespacedType = namespaced + key;
      registerGetter(store, namespacedType, getter, module);
    });
  }

  // 4. 递归安装子模块
  module.forEachChild((child, key) => {
    const childState = child.state;
    // 使用 Vue.set 或 store.registerModule注册子模块
    store.state[key] = childState;
    installModule(store, rootState, path.concat(key), child, hot);
  });
}

这个函数接收四个参数:

  • storeStore 实例。
  • rootState:根状态对象。
  • path:当前模块的路径,是一个数组,表示从根模块到当前模块的路径。
  • module:当前模块对象。
  • hot:是否热更新

installModule 函数的主要工作如下:

  • 获取命名空间: store._modules.getNamespace(path) 用于获取当前模块的命名空间。命名空间可以避免不同模块之间的 mutationsactionsgetters 发生冲突。

  • 注册 mutations: 遍历 module.mutations 对象,将每个 mutation 注册到 store._mutations 对象中。注册的时候,会加上命名空间,避免冲突。

  • 注册 actions: 遍历 module.actions 对象,将每个 action 注册到 store._actions 对象中。注册的时候,也会加上命名空间,避免冲突。如果 action 对象中定义了 root: true,则表示该 action 属于根级别的 action,不需要加上命名空间。

  • 注册 getters: 遍历 module.getters 对象,将每个 getter 注册到 store._wrappedGetters 对象中。注册的时候,也会加上命名空间,避免冲突。

  • 递归安装子模块: 遍历 module.children 对象,递归调用 installModule 函数来安装子模块。

  1. registerMutationregisterActionregisterGetter:注册三剑客

接下来,我们看一下 registerMutationregisterActionregisterGetter 这三个函数,它们分别负责注册 mutationsactionsgetters

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

function registerAction(store, type, handler, local) {
  const entry = store._actions[type] || (store._actions[type] = []);
  entry.push(function wrappedActionHandler(payload) {
    let res = handler.call(store, {
      commit: store.commit,
      dispatch: store.dispatch,
      state: local.state,
      getters: local.getters,
      rootState: store.state,
      rootGetters: store.getters
    }, payload);
    if (!isPromise(res)) {
      res = Promise.resolve(res);
    }
    return res;
  });
}

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

这三个函数的作用很简单,就是将 mutationsactionsgetters 存储到 store 对象的对应属性中。

  • registerMutation:mutation 函数包装成一个函数,然后存储到 store._mutations 对象中。包装后的函数会调用原始的 mutation 函数,并且传入 statepayload 作为参数。

  • registerAction:action 函数包装成一个函数,然后存储到 store._actions 对象中。包装后的函数会调用原始的 action 函数,并且传入一个包含 commitdispatchstategettersrootStaterootGetters 属性的对象作为参数。action 必须返回一个 Promise。

  • registerGetter:getter 函数包装成一个函数,然后存储到 store._wrappedGetters 对象中。包装后的函数会调用原始的 getter 函数,并且传入 stategettersrootStaterootGetters 作为参数。

  1. resetStoreVM:创建 Vue 实例

最后,我们来看一下 resetStoreVM 函数,它负责创建 Vue 实例,并将 stategetters 挂载到 Vue 实例上。

import { computed, reactive } from 'vue'

function resetStoreVM(store, state) {
  const wrappedGetters = store._wrappedGetters
  const computedGetters = {}
  store.getters = {}
  // 将getter转换为computed属性
  Object.keys(wrappedGetters).forEach(key => {
    const fn = wrappedGetters[key]
    computedGetters[key] = computed(() => fn(store))
    Object.defineProperty(store.getters, key, {
      get: () => computedGetters[key].value,
      enumerable: true // for local getters
    })
  })

  // 使用 Vue 提供的 reactive API 使 state 具有响应式
  store._state = reactive(state)
}

这个函数的主要工作如下:

  • 处理 getters: 循环注册的getters,利用computed函数将getter转换为可计算属性,并且挂载到store.getters对象上。

  • 使 state 具有响应式: 使用 Vue 提供的 reactive API 将 state 对象转换成一个响应式对象。这样,当 state 对象发生变化时,所有依赖于 state 的组件都会自动更新。

总结:Store 初始化的全貌

到这里,我们已经把 Store 类的初始化过程扒了个底朝天。现在,让我们来回顾一下整个过程:

  1. 创建 Store 实例时,会接收一个 options 对象,这个对象包含了 stategettersmutationsactions 等配置项。
  2. Store 类会根据 options 对象,初始化 statemutationsactionsgetters
  3. installModule 函数负责递归地安装模块,注册 mutationsactionsgetters
  4. registerMutationregisterActionregisterGetter 这三个函数分别负责注册 mutationsactionsgetters
  5. resetStoreVM 函数负责创建 Vue 实例,并将 stategetters 挂载到 Vue 实例上。

为了更清晰地展示这个过程,我们可以用一个表格来总结一下:

步骤 描述 涉及的函数
1. 构造函数 接收 options 对象,初始化 strict_state_mutations_actions_wrappedGetters_modules Store constructor
2. 安装模块 递归地安装模块,注册 mutationsactionsgetters installModule
3. 注册三剑客 分别负责注册 mutationsactionsgetters registerMutationregisterActionregisterGetter
4. 创建 Vue 实例 创建 Vue 实例,并将 stategetters 挂载到 Vue 实例上。 resetStoreVM

举个栗子:代码演示

为了让大家更好地理解 Store 类的初始化过程,我们来举个栗子:

import { createStore } from 'vuex'

const store = createStore({
  state: () => ({
    count: 0
  }),
  getters: {
    doubleCount: (state) => state.count * 2
  },
  mutations: {
    increment(state) {
      state.count++
    }
  },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => {
        commit('increment')
      }, 1000)
    }
  }
})

export default store

在这个例子中,我们创建了一个简单的 Vuex 实例,它包含了一个 state、一个 getter、一个 mutation 和一个 action

当我们创建 store 实例时,Vuex 会按照上面介绍的步骤,将 stategettermutationaction 注册到 store 对象中。

然后,我们就可以在组件中使用 this.$store.state.count 来访问 state,使用 this.$store.getters.doubleCount 来访问 getter,使用 this.$store.commit('increment') 来提交 mutation,使用 this.$store.dispatch('incrementAsync') 来分发 action

总结:Vuex 的魅力

通过上面的分析,我们可以看到,Vuex 的初始化过程虽然有点复杂,但是它的设计思想非常清晰。它将状态管理的核心逻辑封装在 Store 类中,并且提供了一套规则来修改状态,这使得我们可以更好地管理 Vue 应用的状态,避免状态混乱和难以维护的问题。

Vuex 的魅力在于它的简单易用、可扩展性和可维护性。它不仅可以帮助我们更好地管理 Vue 应用的状态,还可以提高开发效率,降低维护成本。

好了,今天的讲座就到这里。希望大家通过今天的学习,能够对 Vuex 的 Store 类的初始化过程有一个更深入的理解。下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注