本文以
vuex v3.0.1版本进行分析
install
vuex提供了一个 install方法,用于给 vue.use进行注册,install方法对 vue的版本做了一个判断,1.x版本和 2.x版本的插件注册方法是不一样的:
// vuex/src/mixin.js
if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit })
} else {
// override init and inject vuex init procedure
// for 1.x backwards compatibility.
const _init = Vue.prototype._init
Vue.prototype._init = function (options = {}) {
options.init = options.init
? [vuexInit].concat(options.init)
: vuexInit
_init.call(this, options)
}
}
对于 1.x版本,直接将vuexInit方法混入到 vueInit方法中,当 vue初始化的时候,vuex也就随之初始化了
而对于 2.x版本,则是通过混入 mixin的方式,全局混入了一个 beforeCreated钩子函数
这个 vuexInit方法如下:
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
}
}
目的很明确,就是把 options.store 保存在 this.$store中,options就是在 new一个 vue实例的时候,传入的参数集合对象,如果想使用 vuex的话,肯定要把 store传进来的,类似于下面的代码,所以可以拿到 options.store,this指的是当前 Vue实例,这个options.store 就是Store对象的实例,所以可以在组件中通过 this.$store访问到这个 Store实例
const app = new Vue({
el: '#app',
// 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件
store,
components: { Counter },
template: '<div class="app"></div>'
})
Store
上面在 beforeCreate生命周期中会拿到 options.store,这个 store自然也有初始化的过程
每一个 Vuex 应用的核心就是 store,所以需要有 Store的初始化过程,下面是一个最简单的 Store示例(来源于 vuex的官网):
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
}
})
Store的源码位于 vuex/src/store.js,在这个类的 constructor中,new了一个 vue实例,所以vuex可以使用 vue的很多特性,比如数据的响应式逻辑
初始化Store的过程中,其实也是对 modules、dispatch、commit等进行了初始化操作
初始化module,构建module tree
在 Store类的初始化函数 constructor中,下面这句就是 modules的初始化入口:
this._modules = new ModuleCollection(options)
ModuleCollection是一个 ES6的类
// src/module/module-collection.js
constructor (rawRootModule) {
// register root module (Vuex.Store options)
this.register([], rawRootModule, false)
}
这个类的 constructor中调用了 register方法,第二个参数 rawRootModule就是 Store初始化时传进来参数对象 options
// src/module/module-collection.js
register (path, rawModule, runtime = true) {
// ...省略无关代码
const newModule = new Module(rawModule, runtime)
if (path.length === 0) {
this.root = newModule
} 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) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
register方法中会 new Module,这个 Module就是用来描述单个模块的类,里面定义了与单个模块相关(module)的数据结构(data struct)、属性以及方法,大概如下:

这里面的方法、属性等,都和后续构建 Module Tree有关
由于每个 module都有其自己的 state、namespaced、actions等,所以在初始化 module的过程中,也会给每个 module对象上挂载这些属性或方法,例如,下面就是挂载 state的代码:
// src/module/module.js
this._rawModule = rawModule
const rawState = rawModule.state
// Store the origin module's state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
开始初始化的时候,符合path.length === 0,所以执行 this.root = newMudle,接着遇到了 if (rawModule.modules)这个判断语句,前面说了 rawModule是传入的 options,所以这里的 rawModule.modules就是类似下面示例代码 ExampleA中的modules
/**
* 示例代码 ExampleA
*/
const store = new Vuex.Store({
// ... 省略无关代码
modules: {
profile: {
state: { age: 18 },
getters: {
age (state) {
return state.age
}
}
},
account: {
namespaced: true,
state: {
isAdmin: true,
isLogin: true
},
getters: {
isAdmin (state) {
return state.isAdmin
}
},
actions: {
login ({ state, commit, rootState }) {
commit('goLogin')
}
},
mutations: {
goLogin (state) {
state.isLogin = !state.isLogin
}
},
// 嵌套模块
modules: {
// 进一步嵌套命名空间
myCount: {
namespaced: true,
state: { count: 1 },
getters: {
count (state) {
return state.count
},
countAddOne (state, getters, c, d) {
console.log(123, state, getters, c, d);
return store.getters.count
}
},
actions: {
addCount ({ commit }) {
commit('addMutation')
},
delCount ({ commit }) {
commit('delMutation')
},
changeCount ({ dispatch }, { type } = { type: 1 }) {
if (type === 1) {
dispatch('addCount')
} else {
dispatch('delCount')
}
}
},
mutations: {
addMutation (state) {
console.log('addMutation1');
state.count = state.count + 1
},
delMutation (state) {
state.count = state.count - 1
}
}
},
// 继承父模块的命名空间
posts: {
state: { popular: 2 },
getters: {
popular (state) {
return state.popular
}
}
}
}
}
}
})
所以这个判断是用于处理使用了 module的情况,如果存在 modules,则调用 forEachValue对 modules这个对象进行遍历处理
export function forEachValue (obj, fn) {
Object.keys(obj).forEach(key => fn(obj[key], key))
}
拿到 modules里面存在的所有 module,进行 register操作,这里面的 key就是每个 module的名称,例如 示例代码 ExampleA中的 profile、account
到这里再次调用 this.register方法的时候,path.length === 0就不成立了,所以走 else的逻辑,这里遇到了一个this.get方法:
// src/module/module-collection.js
get (path) {
return path.reduce((module, key) => {
return module.getChild(key)
}, this.root)
}
首先对 path进行遍历,然后对遍历到的项调用 getChild,这个getChild方法是前面 Module类中的方法,用于根据 key,也就是在当前模块中根据模块名获取子模块对象,与之对应的方法是 addChild,是给当前模块添加一个子模块,也就是建立父子间的关联关系:
// src/module/module.js
this._children = Object.create(null)
// ...
addChild (key, module) {
this._children[key] = module
}
// ...
getChild (key) {
return this._children[key]
}
看到这里应该就有点思路了,上述一系列操作实际上就是为了以模块名作为属性 key,遍历所有模块及其子模块,构成一棵以 this.root为顶点的 Modules Tree,画成流程图的话会很清晰:

安装 module tree
上面构建好一棵 module tree之后,接下来就要 install这棵树了
// src/store.js
const state = this._modules.root.state
installModule(this, state, [], this._modules.root)
这个方法里做了很多事情,一个个看
首先是对命名空间 namespaced的处理,如果发现当前 module具有 namespaced属性并且值为 true,则会将其注册到 namespace map,也就是存起来:
const namespace = store._modules.getNamespace(path)
// register in namespace map
if (module.namespaced) {
store._modulesNamespaceMap[namespace] = module
}
其中 getNamespace方法就是 ModuleCollection类上的方法,用于根据 path拼接出当前模块的完整 namespace
getNamespace (path) {
let module = this.root
return path.reduce((namespace, key) => {
module = module.getChild(key)
return namespace + (module.namespaced ? key + '/' : '')
}, '')
}
调用 getNamespace获得命名空间名称,然后将命名空间名称作为 key,将对应的命名空间所指的 module对象作为 value缓存到 store._modulesNamespaceMap上,方便后续根据 namespace查找模块,这个东西是可以通过 this.$store._modulesNamespaceMap取到的,例如,对于 ExampleA中的示例:

接下来是一个判断逻辑,符合 !isRoot && !hot条件才能执行,这里的 isRoot是在 installModule方法的开头定义的:
const isRoot = !path.length
path就是 module tree维护的 module父子关系的状态,当 path.length !== 0时,isRoot就是 true,其实这里就是判断当前安装的模块是不是 root模块,也就是 module tree最顶层的节点,这个节点的 path.length 就是 0
由于 module的安装,在 module tree上就是从父级到子级,一开始执行 installModule方法时,传入的 path为 [],则path.length === 0,所以会执行判断语句里面的代码
设置 state
// src/store.js
if (!isRoot && !hot) {
const parentState = getNestedState(rootState, path.slice(0, -1))
const moduleName = path[path.length - 1]
store._withCommit(() => {
Vue.set(parentState, moduleName, module.state)
})
}
调用了 getNestedState:
function getNestedState (state, path) {
return path.length
? path.reduce((state, key) => state[key], state)
: state
}
这里实际上是通过一层层 path.reduce来查找最终的子模块的 state
例如,对于 account/myCount下的 state来说,它的 path是 ['account', 'myCount'],全局 state结构如下:
{
profile: { ... },
account: {
isAdmin: true,
isLogin: true,
// 这是子模块 myCount的命名空间
myCount: {
// 这是子模块myCount的state
count: 1
},
posts: {
popular: 2
}
}
}
当对这个全局 state 和 path = ['account', 'myCount']调用 getNestedState方法时,最终将得到 /myCount的 state:
{
count: 1
}
查找到具体子模块的 state后,挂载到 store._withCommit上,至于为什么挂到这上,这里暂且不分析,后面会说到
构建本地上下文
接下来会执行一个 makeLocalContext方法:
const local = module.context = makeLocalContext(store, namespace, path)
关于这个方法的作用,在它的注释上已经大概描述了一遍:
/**
* make localized dispatch, commit, getters and state
* if there is no namespace, just use root ones
*/
function makeLocalContext (store, namespace, path) {
// ...
}
大概意思就是,本地化 dispatch 、 commit、 getter、state,如果(当前模块)没有 namespace,则直接挂载到 root module上
可能还是不太明白说的是什么意思,实际上,这就是对命名空间模块的一个处理,是为了在调用相应模块的 dispatch、commit、getters以及 state的时候,如果模块使用用了命名空间,则自动在路径上追加上 namespace
比如,对于 dispath而言,如果当前模块存在 namespace,则在调用这个模块的 dispatch方法的时候,把 namespace拼接到 type上,然后根据这个拼接之后的 type来查找 store上的方法并执行:
// makeLocalContext
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)
}
例如,对于 ExampleA代码而言,想要改变 account/myCount下的 count值,可以直接全局调用 this.$store.dispatch('account/myCount/changeCount'), 当 type = 1的时候又会执行 dispatch('addCount'),这个 dispatch其实是想要执行 account/myCount模块下的 addCount这个 actions,而不是 root module下的 addCount
于是,这里就进行了一个全路径 type的拼接,将当前模块的 namespace和 type拼接到一起,即 account/myCount/ 与 addCount的拼接,最后就拼接成了 account/myCount/addCount,正是我们想要的 path,最后将这个全路径 type作为参数传给 store.dispatch方法,这个过程主要是简化了嵌套 module路径的拼接
commit的逻辑与此类似,不过 getter 和 state 就有点不一样了
// src/store.js
// makeLocalContext
Object.defineProperties(local, {
getters: {
get: noNamespace
? () => store.getters
: () => makeLocalGetters(store, namespace)
},
state: {
get: () => getNestedState(store.state, path)
}
})
对于 getter,如果没有 namspace则直接返回 store.getters,否则就调用 makeLocalGetters:
// src/store.js
function makeLocalGetters (store, namespace) {
const gettersProxy = {}
const splitPos = namespace.length
Object.keys(store.getters).forEach(type => {
// skip if the target getter is not match this namespace
if (type.slice(0, splitPos) !== namespace) return
// extract local getter type
const localType = type.slice(splitPos)
// Add a port to the getters proxy.
// Define as getter property because
// we do not want to evaluate the getters in this time.
Object.defineProperty(gettersProxy, localType, {
get: () => store.getters[type],
enumerable: true
})
})
return gettersProxy
}
直接看这段代码可能不太清晰,所以这里带入一个例子看,比如对于 account/myCount/count这个 getter来说(即上述源码中的 type),它的 namespace就是 account/myCount/,它的 localType就是 count,当访问 gettersProxy.count这个 getters的时候,会自动指向全局的 account/myCount/count
然后是 state,调用了 getNestedState,这个方法前面已经说过了,作用和上面的大体一致,就不多说了
另外,这个过程中多次用到 Object.defineProperty来设置给对象上的属性设置 get函数,而不是直接给属性赋值,例如上面的 localType,这种做法的目的在代码上也已经注释得很清楚了,就是为了能够做到在访问的时候才计算值,既减少了实时运算量,主要是又能够保证获取到的值是实时准确的,这个跟 vue的响应式机制相关,这里就不多说了
综上, makeLocalContext这个方法实际上就是做了一个具有命名空间的子模块的 dispatch、commit、getter、state到全局的映射:

vuex的官网在介绍 Actions 这一节的时候,有这么一段话:

其中, Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象这句话里的 context对象指的就是这里本地化的 module对象
注册 mutation action getter
Mutation
首先是 Mutation:
// src/store.js
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
这个 forEachMutation方法是挂在 module实例上的,这个方法没什么好说的,作用就是遍历当前 module上的 mutations,然后将这些 mutation作为参数传入 registerMutation方法中:
// src/store.js
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)
})
}
该方法是给 root store 上的 _mutations[types] 添加 wrappedMutationHandler 方法(至于这个方法是干什么的是另外的问题,这里暂且不去看),而且 store._mutations[type]的值是一个数组,也就是说同一个type 的 _mutations 是可以对应多个 wrappedMutationHandler方法的
例如,对于 ExampleA中的 account/myCount这个 module来说,如果它的 namespaced属性不存在,或者其值是 false,即没有单独的命名空间,然后它的 mutations中又有个叫 goLogin的方法,这个方法在 account这个 module的 mutations中同样存在,于是 state._mutations['account/goLogin']的数组中就存在了两项,一个是 account下的 goLogin方法,一个是 account/myCount下的 goLogin方法
而如果 account/myCount的 namespaced为 true,就不存在这种情况了,因为这个时候,它的goLogin对应的 type是 account/myCount/goLogin
action
// src/store.js
module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key
const handler = action.handler || action
registerAction(store, type, handler, local)
})
逻辑其实和上面的 Mutation差不多,都是遍历所有的 actions,然后挂到 store的某个属性上,只不过 action是挂到 store._actions上,同样的,对于同一个 key,也可以对应多个 action方法,这也跟命名空间有关
getter
// src/store.js
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})
getter和上面的逻辑也都是差不多的,遍历所有的 getter,然后挂到 store的某个属性上,只不过 getter是挂到 store._wrappedGetters上,另外,对于同一个 key,只允许存在一个值,如果存在多个值,则以第二个为准:
// src/store.js
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
)
}
}
最后,如果当前模块具有子模块,则遍历其所有子模块,给这些子模块执行 installModule方法,也就是把上面的步骤再次走一遍
至此,installModule方法就执行完了,这里再回头整体看一遍, 调用 installModule这个方法的时候,代码上面有两行注释:
// src/store.js
// 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 module
同时也会递归地注册所有子 module
并且会将所有 module的 getters 收集到 this._wrappedGetters上
经过上述的分析,再看这段注释,就没什么难以理解的了,这个方法(installModule)就是用于包括子模块在内的所有模块的state、getters、actions、mutations 的一个初始化工作
初始化 store vm
接下来,又执行了 resetStoreVM:
// src/store.js
// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)
这个方法的作用可以从它的注释上大概看出来,初始化 store vm,看到这个 vm我们应该想到 vue的实例 vm,这里实际上就是让 store借助 vue的响应式机制
并且会将 _wrappedGetters注册为 computed的属性,也就是计算属性,_wrappedGetters前面已经提到过了,就是各个模块的 getters的集合,计算属性在 vue中的特性之一是 计算属性是基于它们的依赖进行缓存的。只在相关依赖发生改变时它们才会重新求值,也就是做到了 高效地实时计算,这里就是想让 store上各个模块的 getters也具备这种特性
// src/store.js
// resetStoreVM
store.getters = {}
const wrappedGetters = store._wrappedGetters
const computed = {}
forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
computed[key] = () => fn(store)
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})
使用 forEachValue来遍历 _wrappedGetters,forEachValue前面也提到过了,所以这里的 fn(store)实际上就是:
store._wrappedGetters[type] = function wrappedGetter (store) {
return rawGetter(
local.state, // local state
local.getters, // local getters
store.state, // root state
store.getters // root getters
)
}
也就是 wrappedGetter这个函数,返回一个 rawGetter方法执行的结果,这里的 rawGetter可以看作就是 getter计算得到的结果,所以我们在 getter方法的参数中拿到的四个参数指的就是上面四个:
// https://vuex.vuejs.org/zh/api/#getters
state, // 如果在模块中定义则为模块的局部状态
getters, // 等同于 store.getters
rootState // 等同于 store.state
rootGetters // 所有 getters
拿到 getter之后,就把它交给 computed
接下来又定义了一个 Object.defineProperty:
// src、store.js
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
将 store.getters[key]映射到 store._vm[key]上,也就是当访问 store.getters[key]的时候,就相当于获取store._vm[key]的计算值,至于这里的 store_vm又是什么,跟下面的逻辑有关:
// src、store.js
store._vm = new Vue({
data: {
$$state: state
},
computed
})
store._vm实际上就是一个 vue实例,这个实例只有 data和 computed属性,就是为了借助 vue的响应式机制
这里实际上就是建立了一个 state与 getter的映射关系,因为 getter的计算结果肯定依赖于 state的,它们之间必然存在关联的关系,Store类上有个 state的访问器属性:
// src/store.js
get state () {
return this._vm._data.$$state
}
于是 state 到 getter的映射关系流程如下:

接下来是一个用于规范开发方式的逻辑:
// enable strict mode for new vm
if (store.strict) {
enableStrictMode(store)
}
store.strict这里的 strict是需要开发者在初始化 Store的时候显式声明的,一般似乎大家都不怎么关心这个,不过为了更好地遵循 vuex的开发规范,最好还是加上这个属性
enableStrictMode方法如下:
// src/store.js
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 })
}
上面说了,store._vm其实就是一个 vue实例,所以它有 $watch方法,用于检测 this._data.$$state的变化,也就是 state的变化,当 state变化的时候,store._committing的值必须为 true
这个 store._committing的值在 Store的初始化代码中就已经定义了,值默认为 false:
// src/store.js
this._committing = false
这个值的修改是在 _withCommit方法中:
_withCommit (fn) {
const committing = this._committing
this._committing = true
fn()
this._committing = committing
}
确保在执行 fn的时候, this._committing值为 true,然后执行完了再重置回去,这个 _withCommit的执行场景一般都是对 state进行修改,例如 commit:
// src/store.js
commit (_type, _payload, _options) {
// 省略无关代码
// ...
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
// 省略无关代码
// ...
}
enableStrictMode主要就是为了防止不通过 vuex提供的方法,例如 commit、replaceState等,非法修改 state值的情况,在开发环境下会报警告
总结
从上述分析来看,vuex的初始化基本上与 store的初始化紧密相关,store初始化完毕,vuex基本上也就初始化好了,不过过程中涉及到的部分还是比较多的
分析到现在,都是在说初始化,vuex的 api几乎没说上多少,而vuex的能力就是通过 api来体现的,有空再分析下 vuex api相关的吧

本文详细剖析了Vuex 3.0.1版本的初始化流程,包括安装、模块树构建、本地上下文创建及Store VM初始化,揭示了Vuex如何整合Vue的响应式机制。
1247

被折叠的 条评论
为什么被折叠?



