整体概念
import Vue from "vue";
import Vuex from "vuex";
import App from './App.vue'
Vue.use(Vuex);
const store = new Vuex.Store({
state: {
count: 0,
},
mutations: {
increment(state) {
state.count++;
},
},
});
new Vue({
el: '#app',
render: h => h(App)
store
})
这是一个简单的使用的例子,在项目的开始我们需要
- 引入
vuex
,接着通过Vue.use(vuex)
进行插件的注册 - 通过
new Vuex.Store
创建 store 实例 - 把 store 作为 options 的一项配置,最终传入
_init
函数中
通过 vue.use 进行挂载
vue.use 是什么
Vue.use
定义在src/core/global-api/use.js
文件下,是 vue 通过initGlobalAPI
函数中的initUse(Vue)
实现了 Vue 构造函数静态方法的挂载。
Vue.use
函数分为两个部分,第一个部分是防止重复注册,第二个部分是注册 plugin。
-
防止重复注册
首次进入他会判断
this._installedPlugins
如果没有的话,则把这个属性初始化为一个空数组,接着通过我们传入的plugin
参数判断是否存在于this._installedPlugins
,如果存在则返回 this,不进行注册。-
在这里 this 的指向是 vue 构造函数,也就是说,当我们使用 vue.use 最终返回的仍然是 vue 构造函数,这代表了 vue.use 是可以支持链式操作的。
-
如果找到了之前我们已经注册过这个 plugin,那么不会再次进行注册,防止多次注册。
-
-
注册 plugin
首先通过
toArray
函数拿到 arguments 数组,接着通过 unshift 把 this(vue 构造函数)作为数组的第一项,接着他会判断,传入的plugin
的 install 是否是一个函数,如果是一个函数则直接执行,并把新生成的 args 作为参数传入,否则如果 plugin 是一个函数则直接执行。然后把传入的plugin
push 到this._installedPlugins
,作为注册过的缓存。
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;
};
install 函数
vuex 的 install 函数定义在src/store.js
下,首先他会判断Vue && _Vue === Vue
,_Vue
是通过 use 之后,传入的 vue,Vue
是 vuex 定义的全局变量。在调用install
函数的时候,会先进行赋值操作Vue = _Vue
,这样全局任何地方都可以通过 Vue 拿到 vue 的构造函数。对应上边的判断,如果已经赋值,则说明已经通过Vue.use
进行了注册,如果注册过,则会 return,并且报出'[vuex] already installed. Vue.use(Vuex) should be called only once.'
,这样防止用户重复通过 use 注册。install 最后会执行applyMixin(Vue)
。
// src/store.js
export function install(_Vue) {
if (Vue && _Vue === Vue) {
if (__DEV__) {
console.error(
"[vuex] already installed. Vue.use(Vuex) should be called only once."
);
}
return;
}
Vue = _Vue;
applyMixin(Vue);
}
applyMixin 函数
applyMixin
函数定义在src/mixin.js
。他首先拿到 Vue 的版本,如果版本是小于 2 的,那么会把vuexInit
函数,作为调用_init
的参数传入。如果是大于等于 2 版本,则通过Vue.mixin
把vuexInit
函数混入到beforeCreate
中。
vuexInit
函数首先通过this.$options
拿到传入的 options,他会判断是否有options.store
,如果有则为this.$store
挂载options.store
,如果没有options.store
,则会通过options.parent.$store
拿到 store,并挂载到this.$store
。也就是说,根部的 vue 实例,会首次对this.$store
进行挂载,之后的子组件,则通过不断访问父组件的options.parent.$store
进行挂载。
// src/mixin.js
export default function (Vue) {
const version = Number(Vue.version.split(".")[0]);
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);
};
}
/**
* Vuex init hook, injected into each instances init hooks list.
*/
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;
}
}
}
通过 new Vuex.Store 进行 store 实例的创建
class Store 实例化
Store
是定义在src/store.js
下的一个 class,当我们执行 new 的操作,则会去执行其中的constructor
函数。
constructor
函数主要做了
- 初始化许多实例属性(其中最重要的为
this._modules = new ModuleCollection(options)
) - 执行
installModule
函数- 初始化了根部
module
- 递归的注册了所有的子
module
- 把所有
module
的 getter 收集在了this._wrappedGetters
- 初始化了根部
- 执行了
resetStoreVM
函数,始化了 store 的 vm,并把 state,getter 变为了响应式的数据
// src/store.js
export class Store {
constructor (options = {}) {
...
// store internal state
this._committing = false
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)
...
// 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)
...
}
...
}
new ModuleCollection(options)
class ModuleCollection
的constructor
会执行this.register([], rawRootModule, false)
(rawRootModules 为传入的 options)。
register
函数主要做了
- 通过
new Module(rawModule, runtime)
创建Module
实例 - 如果判断传入的
rawModule
有modules
,则遍历modules
,继续调用自身(register
函数) - 通过
path
参数判断当前是否为根部 Module,如果是,则为 Module 实例的 root 属性绑定该 Module 实例,如果是子 Module,首先拿到父 Module,接着通过addChild
方法建立父子关系,生成树状结构Module
。
// src/module/module-collection.js
export default class ModuleCollection {
constructor (rawRootModule) {
// register root module (Vuex.Store options)
this.register([], rawRootModule, false)
}
get (path) {
// 首次调用由于是空数组,则返回this.root,也就是root Module,之后传入会根据传入的路径找到他的parent
return path.reduce((module, key) => {
// module._children[key]
return module.getChild(key)
}, this.root)
}
...
register (path, rawModule, runtime = true) {
if (__DEV__) {
assertRawModule(path, rawModule)
}
const newModule = new Module(rawModule, runtime)
if (path.length === 0) { // 首次进入path为空数组则满足条件
this.root = newModule // 此时root为根部的store创建的Module实例
}
// 如果是在modules调用register函数,则传入的path不会为空数组,则会走到当前的逻辑
else {
// 首先调用this.get slice(0, -1) 排除掉最后一位,首次调用传入的是空数组,拿到this.root
const parent = this.get(path.slice(0, -1))
// 拿到parent之后通过调用addChild => this._children[key] = module
parent.addChild(path[path.length - 1], newModule)
}
// register nested modules
if (rawModule.modules) { // 如果判断传入的结构有modules则执行
// 遍历modules进行register函数的调用,rawChildModule为value,key为modules的item的具体名称
forEachValue(rawModule.modules, (rawChildModule, key) => {
// 当因为modules调用register的时候,传入的第一个参数不再一个空数组,而是会concat 当前modules key的数组
// 而concat方法是不具有副作用的,这样就可以保证递归的调用register函数
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
...
}
也就是说new ModuleCollection(options)
主要是帮助我们生成了 module 实例,并形成了
class module
Module
在实例化的过程中会执行constructor
- 通过
this._rawModule = rawModule
把传入的 store 结构保存 - 拿到当前传入
constructor
的结构,并把根部 state 作为实例属性 state 进行保存
// src/store.js
export default class Module {
constructor (rawModule, runtime) {
this.runtime = runtime // false
// Store some children item
this._children = Object.create(null)
// Store the origin module object which passed by programmer
this._rawModule = rawModule // 保存传入的store结构
const rawState = rawModule.state // 根部state
// Store the origin module's state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
...
}
installModule
把ModuleCollection
实例赋值给this._modules
后,下一步是安装 module。执行installModule(this, state, [], this._modules.root)
传入了 4 个参数
- 第二个参数
state
,为this._modules.root.state
,也就是ModuleCollection
实例的root.state
,root.state
是根部的 Module 实例的 state。 - 第四个参数
this._modules.root
则是根部的 Module 实例
installModule
函数主要分为三个分支
- 当前是根部 Module
- 当前是子 Module,并且 namespaced 为 true
- 当前是子 Module,并且 namespaced 不为 true
installModule
主要做了
- 首先定义了两个变量
- isRoot 变量 => 通过第三个参数 path 数组的 length 来判断当前是否是根部 Module。
- namespace 变量 => 通过
store._modules.getNamespace(path)
拿到对应 path 的 module 的访问路径(例如:modulesA/modulesB
)
- 通过
makeLocalContext
函数,拿到当前模块的 local(访问经过处理成完整路径的 mutation,action,state,getter)makeLocalContext(store, namespace, path)
- 参数
- 第一个参数传入 store 实例
- 第二个参数传入拿到的
namespace
(访问路径) - 第三个参数传入路径数组 path
- 执行
- 定义
local
对象 - 为 local 对象声明
dispatch
和commit
。根据传入的namespace
字段,决定当前环境(Module)的dispatch
和commit
在调用时的完整路径type
,如果有namespace
则type = namespace + type
,这样当我们写 namespace 为 true 的 modules,则不需要在当前 Module 调用 commit 或 dispatch 时,书写完整路径 - 通过
Object.defineProperties
为 local 对象声明getters
和state
的访问方式 - 返回
local
对象
- 定义
- 通过
registerMutation
,registerAction
,registerGetter
,installModule
为当前 Module 实例,注册Mutation
,Action
,Gette
以及子Module
- 如果是
installModule
执行的是子 Module,则会通过store._withCommit
去设置 store 的 state,形成树形结构
// store, state, [], store._modules.root, true
function installModule(store, rootState, path, module, hot) {
const isRoot = !path.length; // 首次path为空数组,则为true
// 根据path,获取当前的完整路径
const namespace = store._modules.getNamespace(path);
// register in namespace map
if (module.namespaced) {
if (store._modulesNamespaceMap[namespace] && __DEV__) {
console.error(
`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join(
"/"
)}`
);
}
store._modulesNamespaceMap[namespace] = module;
}
// 假如当前是子模块
// set state
if (!isRoot && !hot) {
// 拿到父级的state
const parentState = getNestedState(rootState, path.slice(0, -1));
// 拿到模块名称
const moduleName = path[path.length - 1];
store._withCommit(() => {
if (__DEV__) {
if (moduleName in parentState) {
console.warn(
`[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join(
"."
)}"`
);
}
}
// 去设置当前的state到state中
Vue.set(parentState, moduleName, module.state);
});
}
// 首先拿到当前模块的local(访问经过处理成完整路径的mutation,action,state,getter)
const local = (module.context = makeLocalContext(store, namespace, path));
// 为当前模块注册对应的Mutation
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key;
registerMutation(store, namespacedType, mutation, local);
});
// 为当前模块注册对应的Action
module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key;
const handler = action.handler || action;
registerAction(store, type, handler, local);
});
// 为当前模块注册对应的getter
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key;
registerGetter(store, namespacedType, getter, local);
});
// 遍历子module进行模块安装
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot);
});
}
/**
* make localized dispatch, commit, getters and state
* if there is no namespace, just use root ones
*/
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 (__DEV__ && !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 (__DEV__ && !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;
}
resetStoreVM
resetStoreVM
- 获取旧的
store._vm
,通过store._wrappedGetters
拿到所有注册的 getters,定义空的computed
对象 - 遍历
store._wrappedGetters
,拿到注册的 key,和对应的回调函数,注册在 computed 对象上,并通过Object.defineProperty
进行数据劫持,定义 store 的实例getters
属性最终会访问到store._vm[key]
。 - 为 store._vm 赋值
new Vue
,并且定义其中的 data 中的$$state
属性为 state(store.state),computed 为上边定义的computed
对象 - 如果有定义
oldVm
,则在nextTick
(vue 当前队列结束)之后,销毁旧的 vm 实例。
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
...
Vue.config.silent = true
store._vm = new Vue({
data: {
$$state: state
},
computed
})
...
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())
}
}
Store 实例提供给我们使用的 api
state,getters,commit,dispatch
假设我们的结构是这样的
const modulesA = {
namespaced: true,
state: {
count: 1,
},
getters: {
computedConut(state) {
return state.count + 1;
},
},
mutations: {
add(state, num) {
state.count += num;
},
},
actions: {
addCount(context) {
context.commit("add", 2);
},
},
};
const store = new Vuex.Store({
modules: {
modulesA,
},
});
state
假如我们想访问 modulesA 中 state 的属性 count,官方文档给出的 api 是使用this.$store.state.modulesA.count
。
当访问到state
的时候会触发 Store 实例的state
的 get 函数,他会返回this._vm._data.$$state
,store._vm
在执行resetStoreVM
时,会通过new Vue
进行初始化,data 中的$$state
对应的是定义的 state。state 是resetStoreVM
接受的第二个参数,也就是this._modules.root.state
。在最初this._modules.root.state
只代表根部 module 的 state,并没有形成一个嵌套的结构,state tree 的形成是在子模块执行installModule
函数的时候,会通过Vue.set(parentState, moduleName, module.state)
,往 root.state 中,以模块的 key 作为 key,模块的 state 作为 value 进行添加。也就是说最终的$$state: state
对应的 state 是一个树状结构的 state,这样当我们就可以通过state.modulesA.count
拿到modulesA
模块中的 count。如果我们对 state 进行直接修改,比如this.$store.state.moudlesA.count = 4
,那么并不会成功的修改 state,因为state
的 set 函数他并不会去做相应的 state 的修改,而是会在开发模式下报出一个警告。
export class Store {
...
get state () {
return this._vm._data.$$state
}
set state (v) {
if (__DEV__) {
assert(false, `use store.replaceState() to explicit replace store state.`)
}
}
...
}
// src/store.js
function installModule (store, rootState, path, module, hot) {
...
// set state
if (!isRoot && !hot) {
// 拿到父级的state
const parentState = getNestedState(rootState, path.slice(0, -1))
// 拿到模块名称
const moduleName = path[path.length - 1]
store._withCommit(() => {
...
// 去设置当前的state到父级state中
Vue.set(parentState, moduleName, module.state)
})
}
...
}
getters
假如我们想访问modulesA的getters中的computedConut
,官方文档给出的api是使用this.$store.getters['modulesA/computedConut']
。
getter的初始化同样是在resetStoreVM
函数中,首先会定义一个空的computed
对象,然后遍历store._wrappedGetters
(在执行installModule
函数的时候会执行registerGetter
函数,在store._wrappedGetters
挂载我们定义的getter函数),把getters函数作为值,getter函数的key作为computed的key,注册在computed,在遍历的时候还会通过Object.defineProperty
定义了在访问store.getters.xxx
的访问,最终会访问到store._vm[key]
,也就是会访问到store的vm实例上,为什么这么做,是因为接着,他会把computed作为vm实例的computed,这样通过访问this.$store.getters.xxx
就会被代理到了store._vm[key]
也就是我们经过vue实例computed处理过的具体的getters函数。
// src/store.js
function resetStoreVM (store, state, hot) {
...
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
...
store._vm = new Vue({
data: {
$$state: state
},
computed
})
...
}
commit
假如我们想调用modulesA的mutations中的add
函数,来修改我们的state,那么需要执行this.$store.commit('modulesA/add')
。store中的commit函数首先会通过unifyObjectStyle
函数解析我们传入的三个参数,也就是文档中支持两种传入方式
最终拿到三个参数,第一个是type
也就是我们需要调用的mutations的名称,第二个参数是,payload
,也就是我们需要传入所调用mutations的参数,第三个参数是options
(在actions中提交commit可以传入root:true
,让context访问到根部module)。接着commit函数会通过this._mutations[type]
(在registerMutation
函数中通过)拿到对应的mutations函数,然后在_withCommit
函数的包裹下,遍历执行(因为mutations在注册中会注册为一个数组),并把payload
作为参数传入。为什么用_withCommit
函数进行包裹,_withCommit
函数帮我们做了这样一件事他首先把全局的_committing
置为了true,在执行完其中的函数,在把他置为false,在函数resetStoreVM
的执行中,如果传的strict是true,则会执行enableStrictMode
函数,enableStrictMode
函数的目的是通过vm的
$watch
方法对this._data.$$state
进行了监听,当修改的时候,如果他判断_committing
为false,则会报错。也就是说如果通过mutations进行了state的修改,那么是不会报错的,如果我们擅自进行了修改,则会报错。传入root为true可以调用到根部commit的原因是在installModule
函数中执行makeLocalContext
函数时,他定义了namespaced的module下的commit会接受第三个参数,如果第三个参数(options)中有root为true,那么在调用commit的时候,传入的type就是原始定义的type,而不是和namespace拼接之后的type。
// src/store.js
commit (_type, _payload, _options) {
// check object-style commit
const {
type,
payload,
options
} = unifyObjectStyle(_type, _payload, _options)
const mutation = { type, payload }
const entry = this._mutations[type]
...
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
...
}
// src/store.js
_withCommit (fn) {
const committing = this._committing
this._committing = true
fn()
this._committing = committing
}
// src/store.js
function enableStrictMode (store) {
store._vm.$watch(function () { return this._data.$$state }, () => {
if (__DEV__) {
assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
}
}, { deep: true, sync: true })
}
dispatch
假如我们想调用modulesA的actions中的addCount
函数,来提交提个mutation函数,那么需要执行this.$store.dispatch('modulesA/addCount')
。
store中的dispatch函数和commit函数相似,首先通过unifyObjectStyle
对传入的参数进行解析,拿到type和payload参数。同样也会去this._actions[type]
中拿到对应的actions中注册的函数。和commit不同的是,他并不会直接执行,而是会先判断判断拿到的_actions[type]
的length,如果是1则会执行,如果不是1,则会执行Promise.all(entry.map(handler => handler(payload)))
,这是因为actions在注册的时候会通过registerAction
函数进行注册,registerAction
函数中会判断传入的actions是否是一个promise如果不是promise,则会通过res = Promise.resolve(res)
,把他变成一个promise,dispatch函数最终会返回一个promise,其中的reslove的执行时机,正是执行entry
promise的then的时候。也就是说,我们调用dispatch最终会返回一个promise,这个promise触发then的时机,是对应的所有actions执行完的时候。其实dispatch是一个异步的函数,他可以接受一些异步的方法,最终提交mutation来修改state,dispatch很好的帮助我们规范了提交mutations的方式。
// src/store.js
dispatch (_type, _payload) {
// check object-style dispatch
const {
type,
payload
} = unifyObjectStyle(_type, _payload)
const action = { type, payload }
const entry = this._actions[type]
...
const result = entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)
return new Promise((resolve, reject) => {
result.then(res => {
...
resolve(res)
}, error => {
...
reject(error)
})
})
}
// src/store.js
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,
commit: local.commit,
getters: local.getters,
state: local.state,
rootGetters: store.getters,
rootState: store.state
}, payload)
if (!isPromise(res)) {
res = Promise.resolve(res)
}
if (store._devtoolHook) {
...
} else {
return res
}
})
}
registerModule和unregisterModule
vuex还提供了动态对module的注册和注销
- 注册模块
this.$store.registerModule('c', {
namespaced: true,
...
})
this.$store.registerModule('c', {
namespaced: true,
...
})
registerModule
函数首先对拿到的的第一个参数path
做一个类型判断,如果他是string
类型,则会执行path = [path]
把他变成一个数组。接着会判断path是否是一个数组,如果不是一个数组,则报错这是因为registerModule
接下来的第一步是通过this._modules.register
,对module进行注册,而ModuleCollection
实例,接受的path是一个数组类型的。接着会判断path.length > 0
,如果我们第一个参数传入了一个空的字符串或者空数组,那么在执行register
的时候,代表他是一个根部module,而registerModule
函数不允许注册根部的module。判断了前置条件,接着会调用this._modules.register(path, rawModule)
,this._modules
是ModuleCollection
实例,其中的register
方法是用来注册Module
实例的。执行完这一步,module实例创建完毕,接着会通过installModule
安装模块,最终调用resetStoreVM
函数,对store._vm
进行重新的注册(data,computed),最后对旧的store._vm
通过$destroy
进行销毁。
// src/store.js
registerModule (path, rawModule, options = {}) {
if (typeof path === 'string') path = [path]
if (__DEV__) {
assert(Array.isArray(path), `module path must be a string or an Array.`)
assert(path.length > 0, 'cannot register the root module by using registerModule.')
}
this._modules.register(path, rawModule)
installModule(this, this.state, path, this._modules.get(path), options.preserveState)
// reset store to update getters...
resetStoreVM(this, this.state)
}
- 注销模块
this.$store.unregisterModule('c')
this.$store.unregisterModule(['modulesA','c'])
unregisterModule
函数和registerModule
函数相同,也会首先判断传入的path的类型,如果是string,则会执行path = [path]
,接着判断如果不是数组则报错。接着调用this._modules.unregister(path)
,ModuleCollection
实例的unregister
函数,会首先通过传入的paththis.get(path.slice(0, -1))
拿到父module,然后通过path拿到传入的keyconst key = path[path.length - 1]
,拿到key和parent,则执行const child = parent.getChild(key)
,module实例的getChild
函数,实际是获取了当前module的_children
属性,_children
属性是在执行ModuleCollection
实例的register
函数的时候,通过module实例的addChild
进行添加。如果通过key找到了对用的module实例,则会parent.removeChild(key)
,也就是module实例的delete this._children[key]
,通过delete删除对应的父子依赖关系。接着unregisterModule
会通过this._withCommit
去修改vm实例的data,首先通过getNestedState(this.state, path.slice(0, -1))
找到对应的父state,然后调用Vue.delete(parentState, path[path.length - 1])
,删除vm实例中对应的模块的state。删除之后执行resetStore
,resetStore
函数把store实例的_actions
,_mutations
,_wrappedGetters
和_modulesNamespaceMap
都重置为了空,重新通过store实例拿到state,通过调用installModule
和resetStoreVM
函数进行重新安装和重新注册vm实例。
// src/store.js
unregisterModule (path) {
if (typeof path === 'string') path = [path]
if (__DEV__) {
assert(Array.isArray(path), `module path must be a string or an Array.`)
}
this._modules.unregister(path)
this._withCommit(() => {
const parentState = getNestedState(this.state, path.slice(0, -1))
Vue.delete(parentState, path[path.length - 1])
})
resetStore(this)
}
...
function resetStore (store, hot) {
store._actions = Object.create(null)
store._mutations = Object.create(null)
store._wrappedGetters = Object.create(null)
store._modulesNamespaceMap = Object.create(null)
const state = store.state
// init all modules
installModule(store, state, [], store._modules.root, true)
// reset vm
resetStoreVM(store, state, hot)
}
辅助函数
mapState
在调用的时候,我们可以通过往mapState
函数中传递数组或对象获取相应的state。
参数为对象
computed: mapState({
count: state => state.count,
countAlias: 'count',
})
参数为数组
computed: mapState([
// 映射 this.count 为 store.state.count
'count'
])
mapState
函数定义在src文件夹下的helpers.js
文件中,首先最外层包裹了normalizeNamespace
函数,normalizeNamespace
函数他会对拿到的参数进行解析,如果我们只传递了一个参数(数组或对象),那么在执行typeof namespace !== 'string'
的时候会执行其中的逻辑map = namespace;namespace = ''
,这样在真正执行mapState
内部的函数,参数传递就进行了统一处理。如果我们传递了两个参数,那么会判断我们传递的第一个参数namespace
是否结尾是/
,如果不是,会为我们在末尾拼接/
,最终把处理过后的namespace
和map
两个参数传入内部的函数去执行。mapState
内部的函数首先定义一个空的res变量,接着把第二个参数states
作为参数,传入normalizeMap
函数。normalizeMap
函数的作用是把我们传入的数或者对象统一处理为一个数组对象,这个数组对象中key属性对应的value就是我们传入的key(如果是数组,直接把string作为key),然后把value作为数组对象的val。接着对这个数组对象进行遍历,把每一项的key作为res的key,value是定义的mappedState
函数。最终把res对象return出去。也就是说最终当我们执行mapState
辅助函数,那么最终拿到的是一个res,他的key是我们传入的key,value是mappedState
函数,如果我们把他放入computed中,由于computed在访问时,会自动求值,会把我们的value,mappedState
函数作为computed的getter执行。mappedState
函数首先会通过this.$store.state
和this.$store.getters
拿到全局的state和getters,然后会判断namespace
是否为空,如果我们传入了这个字段(代表我们希望获取到modules中的state),那么他首先会把store实例,'mapState'字符串和传入的namespace
作为参数调用getModuleByNamespace
函数。getModuleByNamespace
函数通过传入的namespace
,去访问store._modulesNamespaceMap[namespace]
,store._modulesNamespaceMap
在installModule
函数中判断当前模块是否有namesapced,如果有,会把当前拿到的module注册在store._modulesNamespaceMap
下。也就是说最终getModuleByNamespace
会根据namespace拿到对应的module。拿到对应的module,mappedState
函数接着会通过module.context
拿到对应的state和getters。module.context
是在installModule
函数中,拿到makeLocalContext
函数的结果进行注册的。其中的getter和state是经过处理后的,访问getter其实是最终store.getters[type]
中的getter,state则为store._vm.state
。拿到state和getters最后会判断我们传入的val,如果val是函数,则执行这个函数,并且把state和getters作为两个参数传入,否则返回state[val]
,所以当我们如果写的value是一个函数,那么第一个参数就是当前namespace对应的module的state。
// src/helpers.js
/**
* Reduce the code which written in Vue.js for getting the state.
* @param {String} [namespace] - Module's namespace
* @param {Object|Array} states # Object's item can be a function which accept state and getters for param, you can do something for state and getters in it.
* @param {Object}
*/
export const mapState = normalizeNamespace((namespace, states) => {
const res = {}
...
normalizeMap(states).forEach(({ key, val }) => {
res[key] = function mappedState () {
let state = this.$store.state
let getters = this.$store.getters
if (namespace) {
const module = getModuleByNamespace(this.$store, 'mapState', namespace)
if (!module) {
return
}
state = module.context.state
getters = module.context.getters
}
return typeof val === 'function'
? val.call(this, state, getters)
: state[val]
}
...
})
return res
})
...
/**
* Normalize the map
* normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ]
* normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ]
* @param {Array|Object} map
* @return {Object}
*/
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] }))
}
...
/**
* Search a special module from store by namespace. if module not exist, print error message.
* @param {Object} store
* @param {String} helper
* @param {String} namespace
* @return {Object}
*/
function getModuleByNamespace (store, helper, namespace) {
const module = store._modulesNamespaceMap[namespace]
if (__DEV__ && !module) {
console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
}
return module
}
mapGetters
在调用的时候,我们可以通过往mapGetters
函数中传递数组或对象获取相应的getters。参数为对象
computed: mapState('modulesA/', {
computedcountA: 'computedcount'
})
参数为数组
computed: mapState('modulesA/', ['computedcount'])
mapGetters同样定义在helper文件中,和mapState
相同,他首先通过normalizeNamespace
函数进行包裹(对传入的参数进行统一的处理,最终处理为namespace和map)。接着他也会定义一个res对象,用来获取存储的getters对象 ,并最终返回。通过normalizeMap
函数对传入的map进行统一的处理成一个数组对象后,进行遍历。并把key作为res的key,value为mappedGetter
函数。当我们访问getters的时候,会去执行mappedGetter
函数,mappedGetter
函数中的处理和mappedState
函数有所不同,他首先会判断namespace
,如果有的话会接着通过getModuleByNamespace
函数判断是否可以通过传入的namespace找到对应的module,如果没有对应的module则会结束函数的执行,也是说,如果我们传入了namespace但是却没有找到对应的module,则会结束函数的执行。getModuleByNamespace
函数会在dev模式下,如果没有找到对应的module,会报出这样的警告console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
当我们在开发的过程当中遇到了这样的报错,就说明我们传入的namespace可能是有问题的,vuex并没有通过这个namespace找到对应的modle。mappedGetter
函数最终会返回this.$store.getters[val]
,store.getters
在执行resetStoreVM
函数的时候,在遍历store._wrappedGetters
的时候,通过Object.defineProperty
进行注册的,他的getter为() => store._vm[key]
,也就是store的vm实例的computed。store._wrappedGetters
是在执行installModule
中的registerGetter
函数的时候,进行绑定的。
// src/helpers.js
/**
* Reduce the code which written in Vue.js for getting the getters
* @param {String} [namespace] - Module's namespace
* @param {Object|Array} getters
* @return {Object}
*/
export const mapGetters = normalizeNamespace((namespace, getters) => {
const res = {}
...
normalizeMap(getters).forEach(({ key, val }) => {
// The namespace has been mutated by normalizeNamespace
val = namespace + val
res[key] = function mappedGetter () {
if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
return
}
...
return this.$store.getters[val]
}
...
})
return res
})
mapMutations
在调用的时候,我们可以通过往mapMutations
函数中传递数组或对象获取相应的mutations。
参数为对象
methods: {
...mapMutations('modulesA', {
add(commit, ...rest) {
commit('add', ...rest)
},
addCount:'add'
})
},
参数为数组
methods: {
...mapMutations('modulesA', ['add'])
},
在调用的时候,我们可以通调用mapMutations
,生成组件的mutationn方法,第一个参数和mapState和mapGetters相同都是可选的传入namespace,第二个参数可以是一个数组或者对象,对象的值可以是一个string,也可以是一个function。mapMutations
函数也是通过normalizeNamespace
进行一层传入的参数的处理。处理之后首先也会定义一个对象res,然后通过normalizeMap
函数对传入的map进行处理。之后遍历处理后的map,以mutations的key作为res的key,mappedMutation
函数,作为value,最终返回res。这块的处理和之前的mapState和mapGetters是相同的。当我们调用对用的mutations的时候,会去执行mappedMutation
函数,mappedMutation
函数首先会通过=this.$store.commit
拿到store实例的commit方法,然后会判断是否传入了namespoace,如果传入了namespoace,同样也会通过getModuleByNamespace
函数拿到对应的module,然后重新定义当前的commit为module.context.commit
,也就是说如果没有传入namespace(全局mutations)则会使用全局的commit方法,如果传入了namespace,则会去找到对应module的局部commit方法。module.context.commit
在执行installModule
函数中的makeLocalContext
进行了定义,他会判断当前是否有namespace,如果有的话他会重新定义在执行store.commit的时候,传入的第一个参数type
为拼接了namespace的结果。最后会判断拿到val是否是一个function,如果是一个function,则会去执行这个函数,并且把拿到的commit
(局部或全局commit),和传入的剩余参数作为调用传入的这个函数的参数。也就是说如果我们写的是一个函数,那么函数的第一个参数将会是commit。如果判断不是一个函数,则会执行拿到的commit,并把传入的mutations名称作为第一个参数,传入的其他参数也传入。这样通过局部的commit再去找到完整的当前module的mutations的函数,最终调用。
// src/helpers.js
/**
* Reduce the code which written in Vue.js for committing the mutation
* @param {String} [namespace] - Module's namespace
* @param {Object|Array} mutations # Object's item can be a function which accept `commit` function as the first param, it can accept another params. You can commit mutation and do any other things in this function. specially, You need to pass anthor params from the mapped function.
* @return {Object}
*/
export const mapMutations = normalizeNamespace((namespace, mutations) => {
const res = {}
...
normalizeMap(mutations).forEach(({ key, val }) => {
res[key] = function mappedMutation (...args) {
// Get the commit method from store
let commit = this.$store.commit
if (namespace) {
const module = getModuleByNamespace(this.$store, 'mapMutations', namespace)
if (!module) {
return
}
commit = module.context.commit
}
return typeof val === 'function'
? val.apply(this, [commit].concat(args))
: commit.apply(this.$store, [val].concat(args))
}
})
return res
})
mapActions
在调用的时候,我们可以通过往mapActions
函数中传递数组或对象获取相应的actions。
参数为对象
methods: {
...mapActions('modulesA', {
addCount:'addCount',
async addCount(dispatch) {
await dispatch('addCount')
}
})
参数为数组
methods: {
...mapActions('modulesA', ['addCount'])
},
mapActions函数和mapMutations函数几乎是相同的,只是把commit换成了dispatch
,这里就不在赘述他的实现步骤了。
createNamespacedHelpers
对于组件绑定的上述4个方法,的确帮助我们节省了部分的代码,但是如果还是面临一个问题,如果我们当前的组件需要操作同一个namespace下的state,action,mutation,getter,则需要传入4次namespace,所以createNamespacedHelpers
函数正是为了解决这个问题而存在的。createNamespacedHelpers
函数接受一分为参数namespace
,然后向外暴露了一个对象,这个对象中有4个属性,分别代表mapState
,mapGetters
,mapMutations
,mapActions
,mapState: mapState.bind(null, namespace),
他巧妙的利用了bind函数的特性,把传入的namespace
作为第一个参数,之后当我们再去写参数的时候,就是第二个参数了,这样第一个参数namespace
就被提前定义了。
// src/helpers.js
/**
* Rebinding namespace param for mapXXX function in special scoped, and return them by simple object
* @param {String} namespace
* @return {Object}
*/
export const createNamespacedHelpers = (namespace) => ({
mapState: mapState.bind(null, namespace),
mapGetters: mapGetters.bind(null, namespace),
mapMutations: mapMutations.bind(null, namespace),
mapActions: mapActions.bind(null, namespace)
})
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!