vuex源码解析
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 下来的源码,一步一步分析执行过程。