Vuex version 3.1.3
先看下Vuex仓库的目录结构
- Vuex
- src
- modules
- module-collection.js // 注册module,生成一个module树,建立状态树的关系
- module.js // module实例的构造函数
- plugins
- devtool.js //
- logger.js // Vuex自带的日志插件
- helpers.js // mapState mapGetters mapMutations mapActions 语法糖
- index.esm.js // 入口函数
- index.js // 入口函数
- mixin.js
- store.js // Store构造函数,初始化整个Vuex仓库
- util.js // 工具函数库
Vuex 初始化
在项目中我们使用Vuex通常都会通过 import Vuex from 'Vuex',对应的就是/src/index.js
export default {
Store,
install,
version: '__VERSION__',
mapState,
mapMutations,
mapGetters,
mapActions,
createNamespacedHelpers
}
install
在使用Vuex之前,我们会通过Vue.use(Vuex)去注册Vuex,Vue.use方法注册在vue/src/core/global-api/use.js中
_installedPlugins来缓存已注册的plugin,防止重复注册,然后去执行plugin的install方法
export function initUse (Vue: GlobalAPI) {
Vue.use = function (plugin: Function | Object) {
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
// additional parameters
const args = toArray(arguments, 1)
args.unshift(this)
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
plugin.apply(null, args)
}
installedPlugins.push(plugin)
return this
}
}
Vuex也提供了install方法,定义在/src/store.js中
install方法通过把传入的_Vue赋值给全局的Vue,防止重复注册,Vue在store.js最上面定义了
export function 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
applyMixin(Vue)
}
执行applyMixin方法,实际上是通过beforeCreate钩子,把options.store存到this.$store对象上,定义在src/mixin.js中
export default function (Vue) {
const version = Number(Vue.version.split('.')[0]) // 对vue版本做判断,2.0版本以上通过beforeCreate全局钩子混入
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)
}
}
vuexInit方法实际上是去合并配置项取new Vue时传入的store对象,子组件通过parent.$store获取
/**
* Vuex init hook, injected into each instances init hooks list.
*/
function vuexInit () {
const options = this.$options // 在执行beforeCreate钩子之前去合并配置
// 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
}
}
}
Store 实例化
new Vue的时候会传入store对象,这个就是我们在后面拿到的options.store
cosnt store = new Vuex.store({
state,
getters,
mutations,
actions,
modules
})
new Vue({
store,
...
})
看一下Store的构造函数,定义在src/store.js中
- _committing 在提交commit时用来锁定状态,防止手动去修改state,dev时报错提醒
- _actions 收集注册的actions
- _mutations 收集注册的mutations
- _wrappedGetters 收集注册的getters
- ModuleCollection 建立module的树,维护modules的关系
- _modulesNamespaceMap 建立命名空间的modules的映射
- _subscribers 用于订阅状态更新
- _watcherVM
- _makeLocalGettersCache // getter做了个缓存 防止namespace的重复注册
- installModule // 递归注册所有module的state,getters,mutations,actions,执行makeLocalContext做命名空间的处理
- resetStoreVM 实现Vuex的响应式
export class Store {
constructor (options = {}) {
if (!Vue && typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
...
// store internal state
this._committing = false // 在提交commit时用来锁定状态,防止手动去修改state,dev时报错提醒
this._actions = Object.create(null)
this._actionSubscribers = []
this._mutations = Object.create(null)
this._wrappedGetters = Object.create(null)
this._modules = new ModuleCollection(options)
this._modulesNamespaceMap = Object.create(null)
this._subscribers = []
this._watcherVM = new Vue()
this._makeLocalGettersCache = Object.create(null)
...
// bind commit and dispatch to self
const store = this
const { dispatch, commit } = 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)
}
const state = this._modules.root.state
// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)
// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)
...
}
}
ModuleCollection
store可以看做是一个root module,在根module下的modules可以看做是子module,Vuex通过ModuleCollection把我们传入的参数对象构造成一个module树状结构,方法定义在src/module/module-collection.js
export default class ModuleCollection {
constructor (rawRootModule) {
// register root module (Vuex.Store options)
this.register([], rawRootModule, false)
}
get (path) {
return path.reduce((module, key) => {
return module.getChild(key)
}, this.root)
}
getNamespace (path) {
let module = this.root
return path.reduce((namespace, key) => {
module = module.getChild(key)
return namespace + (module.namespaced ? key + '/' : '')
}, '')
}
update (rawRootModule) {
update([], this.root, rawRootModule)
}
register (path, rawModule, runtime = true) {
if (process.env.NODE_ENV !== 'production') {
assertRawModule(path, rawModule)
}
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)
})
}
}
unregister (path) {
const parent = this.get(path.slice(0, -1))
const key = path[path.length - 1]
if (!parent.getChild(key).runtime) return
parent.removeChild(key)
}
}
执行new ModuleCollection会去注册module,传入的第一个参数是一个空数组用于维护module的一个层级关系,第二个参数rawModule就是我们传入的参数对象,runtime默认false
- 首次进入path数组长度为0,创建根节点root
- 判断当前module是否存在子module,循环注册子module,path去拼接module的key,即namespace
- 有子module的时候,第二次进入,path长度不为0,执行else,通过get去获取父module,获取父module的方式很简单,通过path.slice(0, -1)即数组最后一个的上一个值,然后通过reduce去获取,传入的默认是this.root,即当前根节点,显然第二次进去当前module是没有子module的,就返回this.root
- 最后通过addChild把当前module添加到父module上
- 继续判断当前module是否有子module
register (path, rawModule, runtime = true) {
if (process.env.NODE_ENV !== 'production') {
assertRawModule(path, rawModule)
}
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)
})
}
}
这里做了提供了一个例子,通过path.slice(0, -1)可以取到父module
/**
* 对于module a -> c ['a', 'c']
* 对于module b -> d -> e ['b', 'd', 'e']
* 对于module b -> g ['b', 'g']
* 对于module f -> g ['g', 'g']
*/
var root = {
modules: {
a: {
modules: {
c: {}
}
},
b: {
modules: {
d: {
modules: {
e: {}
}
},
g: {
}
}
},
f: {
modules: {
g: {}
}
}
}
}
class ModuleCollection {
constructor (path, rawModule) {
this.register(path, rawModule)
}
register (path, rawModule) {
console.log(path);
if (rawModule.modules) {
Object.keys(rawModule.modules).forEach(key => {
this.register(path.concat(key), rawModule.modules[key])
})
}
}
}
/**
*
*/
const root = {
children: {
a: {
state: 1,
children: {
b: {
state: 2,
children: {
c: {
state: 3,
chilrend: {
d: {
state: 4
}
}
}
}
}
}
}
}
}
const path = ['a', 'b', 'c', 'd']
const get = path => {
return path.reduce((module, key) => module.children[key], root)
}
get(path.slice(0, -1)) // 可以取到d的父module
Module
方法定义在src/module/module.js中,主要是创建module实例
- runtime
- _children 存储子module,维护父子module的关系
- _rawModule 缓存当前module
- state 当前module的state
- namespaced 当前是否为命名空间的module
- addChild 添加子module
- removeChild 移除子module
- getChild 获取子module
- update 更新_rawModule
export default class Module {
constructor (rawModule, runtime) {
this.runtime = runtime
// Store some children item
this._children = Object.create(null)
// Store the origin module object which passed by programmer
this._rawModule = rawModule
const rawState = rawModule.state
// Store the origin module's state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
get namespaced () {
return !!this._rawModule.namespaced
}
addChild (key, module) {
this._children[key] = module
}
removeChild (key) {
delete this._children[key]
}
getChild (key) {
return this._children[key]
}
update (rawModule) {
this._rawModule.namespaced = rawModule.namespaced
if (rawModule.actions) {
this._rawModule.actions = rawModule.actions
}
if (rawModule.mutations) {
this._rawModule.mutations = rawModule.mutations
}
if (rawModule.getters) {
this._rawModule.getters = rawModule.getters
}
}
forEachChild (fn) {
forEachValue(this._children, fn)
}
forEachGetter (fn) {
if (this._rawModule.getters) {
forEachValue(this._rawModule.getters, fn)
}
}
forEachAction (fn) {
if (this._rawModule.actions) {
forEachValue(this._rawModule.actions, fn)
}
}
forEachMutation (fn) {
if (this._rawModule.mutations) {
forEachValue(this._rawModule.mutations, fn)
}
}
}
installModule
递归注册所有module,初始化state,getters,mutations,actions
/**
* store 当前store实例
* rootState 根节点的state
* path 维护父子module关系的key数组
* module 根module
* hot
*/
function installModule (store, rootState, path, module, hot) {
const isRoot = !path.length
const namespace = store._modules.getNamespace(path)
// register in namespace map
if (module.namespaced) {
if (store._modulesNamespaceMap[namespace] && process.env.NODE_ENV !== 'production') {
console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
}
store._modulesNamespaceMap[namespace] = module
}
// set state
if (!isRoot && !hot) {
const parentState = getNestedState(rootState, path.slice(0, -1))
const moduleName = path[path.length - 1]
store._withCommit(() => {
if (process.env.NODE_ENV !== 'production') {
if (moduleName in parentState) {
console.warn(
`[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('.')}"`
)
}
}
Vue.set(parentState, moduleName, module.state)
})
}
const local = module.context = makeLocalContext(store, namespace, path)
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key
const handler = action.handler || action
registerAction(store, type, handler, local)
})
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
}
makeLocalContext
返回ModuleCollection中看一下namaspace是怎么获取的
getNamespace (path) {
let module = this.root
return path.reduce((namespace, key) => {
module = module.getChild(key)
return namespace + (module.namespaced ? key + '/' : '')
}, '')
}
还是老样子举个例子
let root = {
namespaced: false,
state: 1,
children: {
a: {
namespaced: true,
state: 2,
children: {
b: {
namespaced: true,
state: 3,
children: {
c: {
namespaced: true,
children: {
d: {
namespaced: true,
state: 4
}
}
}
}
}
}
},
b: {
namespaced: true,
state: 5,
children: {
e: {
namespaced: true,
state: 6
}
}
}
}
}
const path = ['a', 'b', 'c', 'd']
let module = root
const getNamespace = path => path.reduce((namespace, key) =>
(module = module.children[key], namespace + (module.namespaced ? key + '/' : '')), '')
getNamespace(path) // a/b/c/d/
拼接namespace,返回一个local对象,如果不存在命名空间则返回store.dispath,否则把namespace拼接进去
function makeLocalContext (store, namespace, path) {
const noNamespace = namespace === ''
const local = {
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)
},
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
}
makeLocalGetters,先通过_makeLocalGettersCache缓存防止重复监听,
function makeLocalGetters (store, namespace) {
if (!store._makeLocalGettersCache[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
})
})
store._makeLocalGettersCache[namespace] = gettersProxy
}
return store._makeLocalGettersCache[namespace]
}
getNestedState通过reduce逐级获取,state的定义跟getters不同,getters是通过 'a/b/c' 去读取key,而state是对象形式,因此只需要根据path去获取即可
function getNestedState (state, path) {
return path.reduce((state, key) => state[key], state)
}
registerMutation 和 registerAction
注册mutation和action时会把store实例传入,type即带命名空间的属性,handler即执行函数,local通过makeLocalContext转换过得对象,分别存到_mutations和_actions中
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, {
dispatch: local.dispatch, // 我们在执行action函数式可以拿到转换后的dispatch
commit: local.commit,
getters: local.getters,
state: local.state, // 命名空间的state
rootGetters: store.getters, // 根module的getters
rootState: store.state // 根module的state
}, payload)
if (!isPromise(res)) { // 判断是否支持promise
res = Promise.resolve(res) // 返回promise
}
if (store._devtoolHook) {
return res.catch(err => {
store._devtoolHook.emit('vuex:error', err)
throw err
})
} else {
return res
}
})
}
registerGetter
先判断是getter是否重复,然后getters存在_wrappedGetters上,rawGetter即我们定义的回调函数
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
)
}
}
resetStoreVM
主要通过store._vm = new Vue({ data: {
?state: state
},
computed
}) 实现数据的响应式更新,我们在读取state时其实就是访问store._vm.?state
get state () {
return this._vm._data.$$state
}
function resetStoreVM (store, state, hot) {
const oldVm = store._vm
// bind store public getters
store.getters = {}
// reset local getters cache
store._makeLocalGettersCache = Object.create(null)
const wrappedGetters = store._wrappedGetters
const computed = {}
forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
// direct inline function use will lead to closure preserving oldVm.
// using partial to return function with only arguments preserved in closure environment.
computed[key] = partial(fn, store)
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
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
if (store.strict) {
enableStrictMode(store)
}
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())
}
}
Vuex语法糖
我们在使用mapState等方法时可以支持命名空间定义,也可以直接通过/定义,定义在src/helper.js中
我们可以传数组,也可以传对象
Vuex.mapState('a', {
acount: state => state.count,
f: state => state.f
})
Vuex.mapState('a/acount')
Vuex.mapState(['a'])
Vuex.mapState({a: state => state.count})
主要是通过normalizeNamespace和normalizeMap两个方法做了转换
function normalizeNamespace (fn) {
return (namespace, map) => {
if (typeof namespace !== 'string') {
map = namespace
namespace = ''
} else if (namespace.charAt(namespace.length - 1) !== '/') {
namespace += '/'
}
return fn(namespace, map)
}
}
function normalizeMap (map) {
if (!isValidMap(map)) {
return []
}
return Array.isArray(map)
? map.map(key => ({ key, val: key }))
: Object.keys(map).map(key => ({ key, val: map[key] }))
}
之前在store初始化时定义的_modulesNamespaceMap,通过获取命名空间的映射返回
function getModuleByNamespace (store, helper, namespace) {
const module = store._modulesNamespaceMap[namespace]
if (process.env.NODE_ENV !== 'production' && !module) {
console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
}
return module
}