vuex简介
能看到此文章的人,应该大部分都已经使用过vuex了,想更深一步了解vuex的内部实现原理。所以简介就少介绍一点。官网介绍说Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。数据流的状态非常清晰,按照 组件dispatch Action -> action内部commit Mutation -> Mutation再 mutate state 的数据,在触发render函数引起视图的更新。附上一张官网的流程图及vuex的官网地址:https://vuex.vuejs.org/zh/
Questions
在使用vuex的时候,大家有没有如下几个疑问,带着这几个疑问,再去看源码,从中找到解答,这样对vuex的理解可以加深一些。
- 官网在严格模式下有说明:在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。vuex是如何检测状态改变是由mutation函数引起的?
- 通过在根实例中注册 store 选项,该 store 实例会注入到根组件下的所有子组件中。为什么所有子组件都可以取到store?
- 为什么用到的属性在state中也必须要提前定义好,vue视图才可以响应?
- 在调用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 中解构出原型上的 dispatch
和 commit
方法,并进行二次包装,将 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 下来的源码,一步一步分析执行过程。