各位观众老爷,晚上好!今天咱们来聊聊 Vuex 的老底儿,扒一扒 Store 类的初始化过程,看看 Vuex 是怎么把 state、getters、mutations、actions 这些宝贝疙瘩安排得井井有条的。 咱们尽量用大白话,加上点小幽默,保证大家听得懂、记得住。
开场白:Vuex 的本质是什么?
在深入源码之前,咱们先来明确一个概念:Vuex 其实就是一个状态管理容器,它把应用的状态集中管理起来,并且提供了一套规则来修改状态。可以把 Vuex 看作一个全局的“数据库”,专门用来存放和管理 Vue 应用的数据。
Store 类:Vuex 的心脏
Store 类是 Vuex 的核心,所有的状态管理逻辑都围绕着它展开。我们创建 Vuex 实例的时候,实际上就是在创建一个 Store 类的实例。
初始化过程:一步一个脚印
接下来,咱们就一步一个脚印,来剖析 Store 类的初始化过程。为了方便理解,咱们以 Vue 3 版本的 Vuex 为例,但 Vue 2 版本的思路也是大同小异,稍微改改就能用。
- 构造函数: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 对象,这个对象包含了 state、getters、mutations、actions 等配置项。 让我们逐行解读一下:
-
strict: 严格模式开关。开启严格模式后,只有通过 mutation 才能修改 state,否则会抛出错误。这有助于我们在开发过程中避免意外的状态修改。 -
this._state:state是 Vuex 存储数据的核心。如果state是一个函数,就调用它来获取初始状态;否则,直接使用传入的state对象。 这样做的好处是,如果state是一个函数,每次创建Store实例时,都会得到一个新的state对象,避免多个组件共享同一个state对象导致数据污染。 -
this._mutations、this._actions、this._wrappedGetters: 这三个对象分别用于存储mutations、actions和getters。Object.create(null)创建一个没有原型的空对象,避免原型链上的属性干扰。 -
this._modules: 用于处理模块化的情况。ModuleCollection类负责将模块化的options对象转换成一个树形结构,方便后续的安装。 -
installModule: 这个函数是整个初始化过程的核心。它递归地安装模块,注册mutations、actions和getters。 -
resetStoreVM: 这个函数负责创建 Vue 实例,并将state和getters挂载到 Vue 实例上。这样,我们就可以在组件中通过this.$store.state和this.$store.getters来访问状态和计算属性了。 -
plugins: Vuex 允许我们使用插件来扩展功能。plugins数组中的每个插件都会被调用,并且传入Store实例作为参数。 -
devtools: 如果安装了 Vue Devtools,Vuex 会通过devtools对象与 Devtools 进行通信,方便我们调试 Vuex 应用。
installModule:模块安装的关键
接下来,我们重点看一下 installModule 函数,它是注册 mutations、actions 和 getters 的关键。
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);
});
}
这个函数接收四个参数:
store:Store实例。rootState:根状态对象。path:当前模块的路径,是一个数组,表示从根模块到当前模块的路径。module:当前模块对象。hot:是否热更新
installModule 函数的主要工作如下:
-
获取命名空间:
store._modules.getNamespace(path)用于获取当前模块的命名空间。命名空间可以避免不同模块之间的mutations、actions和getters发生冲突。 -
注册
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函数来安装子模块。
registerMutation、registerAction、registerGetter:注册三剑客
接下来,我们看一下 registerMutation、registerAction 和 registerGetter 这三个函数,它们分别负责注册 mutations、actions 和 getters。
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
);
}
}
这三个函数的作用很简单,就是将 mutations、actions 和 getters 存储到 store 对象的对应属性中。
-
registerMutation: 将mutation函数包装成一个函数,然后存储到store._mutations对象中。包装后的函数会调用原始的mutation函数,并且传入state和payload作为参数。 -
registerAction: 将action函数包装成一个函数,然后存储到store._actions对象中。包装后的函数会调用原始的action函数,并且传入一个包含commit、dispatch、state、getters、rootState和rootGetters属性的对象作为参数。action必须返回一个 Promise。 -
registerGetter: 将getter函数包装成一个函数,然后存储到store._wrappedGetters对象中。包装后的函数会调用原始的getter函数,并且传入state、getters、rootState和rootGetters作为参数。
resetStoreVM:创建 Vue 实例
最后,我们来看一下 resetStoreVM 函数,它负责创建 Vue 实例,并将 state 和 getters 挂载到 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 提供的
reactiveAPI 将state对象转换成一个响应式对象。这样,当state对象发生变化时,所有依赖于state的组件都会自动更新。
总结:Store 初始化的全貌
到这里,我们已经把 Store 类的初始化过程扒了个底朝天。现在,让我们来回顾一下整个过程:
- 创建
Store实例时,会接收一个options对象,这个对象包含了state、getters、mutations、actions等配置项。 Store类会根据options对象,初始化state、mutations、actions和getters。installModule函数负责递归地安装模块,注册mutations、actions和getters。registerMutation、registerAction和registerGetter这三个函数分别负责注册mutations、actions和getters。resetStoreVM函数负责创建 Vue 实例,并将state和getters挂载到 Vue 实例上。
为了更清晰地展示这个过程,我们可以用一个表格来总结一下:
| 步骤 | 描述 | 涉及的函数 |
|---|---|---|
| 1. 构造函数 | 接收 options 对象,初始化 strict、_state、_mutations、_actions、_wrappedGetters 和 _modules。 |
Store constructor |
| 2. 安装模块 | 递归地安装模块,注册 mutations、actions 和 getters。 |
installModule |
| 3. 注册三剑客 | 分别负责注册 mutations、actions 和 getters。 |
registerMutation、registerAction、registerGetter |
| 4. 创建 Vue 实例 | 创建 Vue 实例,并将 state 和 getters 挂载到 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 会按照上面介绍的步骤,将 state、getter、mutation 和 action 注册到 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 类的初始化过程有一个更深入的理解。下次再见!