Vuex - 源码概览
栏目: JavaScript · 发布时间: 5年前
内容简介:对于这个
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
相关的吧
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- groupcache源码解析-概览
- React事件机制 - 源码概览(上)
- React事件机制 - 源码概览(下)
- vue-router 源码概览
- mybatis 源码分析(一)框架结构概览
- WebRTC Native 源码导读(十四):API 概览
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Head First HTML5 Programming
Eric Freeman、Elisabeth Robson / O'Reilly Media / 2011-10-18 / USD 49.99
What can HTML5 do for you? If you're a web developer looking to use this new version of HTML, you might be wondering how much has really changed. Head First HTML5 Programming introduces the key featur......一起来看看 《Head First HTML5 Programming》 这本书的介绍吧!
Markdown 在线编辑器
Markdown 在线编辑器
HEX HSV 转换工具
HEX HSV 互换工具