前言
vuex作为一款强有力的状态管理工具被广泛应用于实际工作当中,通过学习vuex的源码可以帮助我们解决藏于心中很久的困惑.
比如vuex的全局状态存放到了哪个地方?为什么修改store里面的状态,页面也会同步更新?action、mutation它们是如何协作修改状态的?为什么action里面建议写异步操作,而mutation定义成同步?
很多实际使用过vuex的同学相信心中会存在这些疑问.本文将摒弃从头到尾直述源码的方式,从提出问题再结合场景去研究源码的实现过程,从而将上述疑问一一解答.
核心原理
首先从框架的思维跳出来,我们先实现一个简易版的vuex,直接窥探它如何做到响应式控制页面变化的.
观察下面代码,起始先定义一个Vue实例vm,注意这个vm它只定义了一个data属性,它没有和页面上的dom节点相绑定,也没有创建其他的属性和方法.
Store是一个构造函数,用来创建store对象,它会将vm赋予store对象.
Object.defineProperties在Store构造函数的原型上定义了一个state,并且设置了state的获取函数.
const vm = new Vue({
data:{
value:"hello world"
}
})
function Store(){
this._vm = vm;
}
Object.defineProperties(Store.prototype,{
state:{
configurable:true,
get(){
return this._vm;
}
}
})
const store = new Store(); // 创建一个store对象
从上面代码可以看出,使用Store构造函数创建的store对象使用了一层代理.比如store.state.value会直接从get函数中获取值,而get函数又是从vm中拿值.那么从store.state.value中拿到的值其实是从vm定义的data中拿到的.
vm是一个Vue实例,它定义的状态具有响应式特性,这也就间接使store.state具有响应式.
store对象已经准备好了,接下来要将它注入到页面当中使用.Vue.mixin可以轻松做到这一点(代码如下).
它会在每一个Vue实例创建的过程中添加created生命周期,函数内通过this.$options拿到配置项判端是否传递了store属性,如果包含就赋值给this.$store.
这样在Vue实例内部通过this就可以拿到store对象了.然后我们开始开发页面,使用new新构建一个Vue实例app.
app会被挂载到页面节点#app中,store作为参数赋值,另外计算属性里面定义了一个属性value.
Vue.mixin({
created() {
if(this.$options.store){
this.$store = this.$options.store;
}
}
})
const app = new Vue({
el: '#app',
store,
computed:{
value(){
return this.$store.state.value;
}
}
})
此时页面的模板里填写{{value}}会发现网页渲染出了hello world.我们会惊奇的发现vm定义的状态最终会映射到app的计算属性里.
数据的投射只是一方面,如果此时修改vm.value = hello,页面会重新渲染,内容变成hello.
讲述到这里,vuex的响应式原理已经逐渐清晰.vm和app都是构建出来的Vue实例.
vm的data相当于一个数据仓库,而app会使用仓库的数据.由于vm的data具有响应式,所以对vm的修改也会触发app模板的重新渲染.
上面的场景过于简单,它的数据结构仅仅只是一个对象.在实际开发的需求里,页面的状态要复杂的多,我们通常会将store的状态划分到不同模块中处理.
vm直接修改状态虽然简单,但直接修改太过暴力并且错误不容易追踪,所以也就衍生出了action、mutation来修改状态的方式.
我们接下来深入研究一下vuex如何支持模块化的数据以及状态的修改.
模块化实现
vuex源码定义的Store构造函数如下,首先定义了大量的初始化属性,其中this._modules = new ModuleCollection(options)这一句很关键.
options是开发者定义的配置项,将其传入ModuleCollection生成一个根模块_modules.
紧接着执行了两个很重要的方法.一个是installModule安装模块,另一个是resetStoreVM将状态响应化处理.
var Store = function Store (options) {
var this$1 = this;
...
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();
// bind commit and dispatch to self
var store = this;
var state = this._modules.root.state;
//安装模块
installModule(this, state, [], this._modules.root);
//状态响应化
resetStoreVM(this, state);
...
};
构建模块化对象
new ModuleCollection(options)会构建一个模块对象赋值给this._modules(这个this指向store实例对象).
我们先回顾一下开发者定义的options的模块化配置.
// 导出一个store对象
export default new Vuex.Store({
state,
mutations,
actions,
modules: {
home, // home模块
login // login模块
}
})
在全局下可以配置state,mutations以及actions,在每一个子模块下也能配置state,mutations以及actions.
这里之所以要构建模块对象返回,主要原因是因为开发者定义的配置对象不利于操作和计算,所以要将原始的配置对象转化成另外一种更加方便使用的数据结构.
ModuleCollection对options处理代码如下,它最终会返回一个模块对象.首次调用先执行register注册根模块,此时path是一个空数组.
new Module直接初始化了一个实例对象newModule,它主要包含三个属性:_children,_rawModule和state.
_children:当前模块的子模块_rawModule:当前模块的配置项(开发者定义的option)state:当前模块的数据状态
register函数是核心方法.它会根据配置项构造出模块对象newModule,然后根据path数组的长度来判断当前模块是不是根模块.
如果是根模块,就把newModule赋值给root属性.如果不是根模块,它就会通过path获取当前模块的父模块,再将当前模块赋予父模块的_children上.
再往下判断rawModule.modules是否存在,如果发现还有子模块的配置,继续递归调用register函数.
var ModuleCollection = function ModuleCollection (rawRootModule) {
// rawRootModule对应着options
this.register([], rawRootModule, false);
};
ModuleCollection.prototype.register = function register (path, rawModule, runtime) {
var this$1 = this;
var newModule = new Module(rawModule, runtime);
if (path.length === 0) {
this.root = newModule;
} else {
var parent = this.get(path.slice(0, -1));//根据path获取父模块
parent.addChild(path[path.length - 1], newModule);//将子模块赋予父模块的``_children``上.
}
// register nested modules
if (rawModule.modules) {//发现当前配置存在子模块的配置
forEachValue(rawModule.modules, function (rawChildModule, key) {
//拿出每一个子模块的配置项,递归调用register
this$1.register(path.concat(key), rawChildModule, runtime);
});
}
};
Module.prototype.addChild = function addChild (key, module) {
this._children[key] = module;
};
//每个模块初始化了_children,_rawModule,state三个属性,state的值是从配置项中获取
var Module = function Module (rawModule, runtime) {
this.runtime = runtime;
this._children = Object.create(null);
this._rawModule = rawModule;
var rawState = rawModule.state;
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {};
};
上面这一轮的操作最终的目的就是为了返回一个数据对象,数据结构如下.该条数据包含了父子级的层级结构,每一级都拥有自己的state状态.
{
root:{
{
state: {userInfo: {…}},
_children:{
home: Module {runtime: false, _children: {…}, _rawModule: {…}, state: {…}}
login: Module {runtime: false, _children: {…}, _rawModule: {…}, state: {…}}
},
_rawModule: {state: {…}, mutations: {…}, actions: {…}, modules: {…}},
namespaced: false
}
}
}
返回的数据对象会赋值给this._modules(这个this指向store实例).
模块化处理
this._modules的数据构建完毕,从根模块里取出初始状态state并联合其他参数传入installModule函数执行模块的安装(代码如下).
var Store = function Store (options) {
...
this._modules = new ModuleCollection(options);
...
var state = this._modules.root.state;
installModule(this, state, [], this._modules.root);
...
}
installModule源码如下.模块的安装过程可以分为以下四步.
- 设置状态的层级结构.以根模块的
rootState为初始值,每个子模块以模块名为key,以子模块的数据状态为value赋值到父模块的状态对象上.例如生成的数据结构如下.
store._module = {
root:{
state:{
user_info:null,//user_info是根模块的状态信息
home:{ //list是home模块(子模块)的状态,以子模块的名称为``key``,状态为值赋予父模块的状态对象上
list:[]
}
}
}
}
-
构建当前模块的本地上下文对象.
makeLocalContext函数返回一个local对象,里面包含dispatch,commit,getters和state.返回的local对象赋予模块对象的context. -
以上面
local对象为基础,开始全局注册actions,mutations和getters. -
前三步完成代表当前模块安装完毕.如果发现当前模块含有子模块,则继续递归调用
installModule安装每个子模块.
function installModule (store, rootState, path, module, hot) {
var isRoot = !path.length;//path为空数组时,说明是根模块
//namespace的形式,如果是根模块为空字符串"",有一个home子模块格式为"home/"
var namespace = store._modules.getNamespace(path);
// 使用键值对将module存储起来
if (module.namespaced) {
store._modulesNamespaceMap[namespace] = module;
}
//设置状态的层级结构,根模块不执行
if (!isRoot && !hot) {
//获取父级的state
var parentState = getNestedState(rootState, path.slice(0, -1));
//获取当前模块的name
var moduleName = path[path.length - 1];
store._withCommit(function () {
//将当前模块的状态赋值给父级模块的状态对象上
Vue.set(parentState, moduleName, module.state);
});
}
//构造了当前模块的本地上下文对象,里面包含dispatch,commit,getters和state
//它们都只操作当前模块的内容,local被赋予module.context
var local = module.context = makeLocalContext(store, namespace, path);
//开始注册actions,mutations和getters
module.forEachMutation(function (mutation, key) {
var namespacedType = namespace + key;
registerMutation(store, namespacedType, mutation, local);
});
module.forEachAction(function (action, key) {
var type = action.root ? key : namespace + key;
var handler = action.handler || action;
registerAction(store, type, handler, local);
});
module.forEachGetter(function (getter, key) {
var namespacedType = namespace + key;
registerGetter(store, namespacedType, getter, local);
});
//递归安装子模块,forEachChild内部遍历module._children拿到每个子模块
module.forEachChild(function (child, key) {
installModule(store, rootState, path.concat(key), child, hot);
});
}
现在看一下local对象的组成以及actions,mutations和getters的注册过程.
local对象是运行makeLocalContext(store, namespace, path)返回的结果,它最终会被存储在module.context下面.
makeLocalContext函数体内定义了一个变量名为local的对象,里面包含dispatch和commit两个函数,另外还对local的getters和state做了get配置,最终返回local对象.
从整体上看,local对象对外暴露了四个属性分别是dispatch,commit,getters和state.这四个属性只服务于当前模块.
比如开发者在根模块下配置了一个home模块,那么home模块就会拥有一个属于自己的local对象.此local对象执行dispatch或commit就会只执行home模块内定义的actions和mutations的函数.另外getters和state也指向了home模块内的getters函数和state.
local四个属性的实现后面再讲.主流程获取local对象后开始注册actions,mutations和getters.
先看下面actions的注册过程(代码如下),module.forEachAction拿到的action就是开发者在该模块下定义的action函数.
store是vuex最终返回的实例对象,type对应着action的函数名,如果是在子模块里,type为模块名与函数名拼接的结果,比如home模块下actions定义了一个getList(...){...},对应的type就为home/getList.
registerAction函数正式开始注册actions,上面讲解Store构造函数已经提及了this._actions = Object.create(null)的初始化定义.registerAction函数目的就是在store对象下的_actions里塞进去一个处理函数,对象的key值便是type.
module.forEachAction(function (action, key) {
var type = namespace + key;
var handler = action;
registerAction(store, type, handler, local);
});
function registerAction (store, type, handler, local) {
var entry = store._actions[type] || (store._actions[type] = []);
entry.push(function wrappedActionHandler (payload, cb) {
...
});
}
最后执行的结果便是下面这样.将各个模块下定义的actions函数全部打平放到store对象下的_actions里面.
store = {
...
_actions:{
home/getList: [ƒ], //home模块下的getList函数
home/getNav: [ƒ], // home模块下的getNav函数
getUser: [ƒ] //根模块的getUser函数
}
...
}
我们再看一下塞到全局的actions函数wrappedActionHandler内部的具体实现.它的内部利用闭包关联了全局store对象、当前模块的local对象以及开发者定义的actions函数handler.
wrappedActionHandler函数内部直接执行handler,但是给actions函数传递的dispatch,commit,getters和state都是从local中获取的.这就是为什么开发者在定义模块下的actions函数时,函数的参数dispatch,commit,getters和state只会作用于当前的模块.
function wrappedActionHandler (payload, cb) {
var 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);
}
return res
});
举个例子.比如home模块下的定义了一个getList函数,函数的参数state它指向home模块下的state,而commit则会调用home模块下mutations定义的setList函数,而不会从全局范围内去找.
actions:{ //在action中可以进行ajax请求数据并对数据进行处理
getList({state,commit}){
ajax({id:state.id}).then((data)=>{
commit("setList",data);
})
}
}
上述handler返回的结果会做Promise化处理,这就意味着wrappedActionHandler返回的结果一定是一个Promise.
action的注册已经介绍完毕,说到底就是将各个模块定义的actions函数全部提取出来做一层封装处理塞到全局store对象下的_actions属性里面.
我们再来看看mutations和getters的注册.它们做的处理方式和actions类似,各个模块定义的mutations函数全部提取出来做一层封装处理塞到全局store对象下的_mutations中,而getter则塞到store对象下的_wrappedGetters里.
module.forEachMutation(function (mutation, key) {
var namespacedType = namespace + key;
registerMutation(store, namespacedType, mutation, local);
});
module.forEachGetter(function (getter, key) {
var namespacedType = namespace + key;
registerGetter(store, namespacedType, getter, local);
});
//注册mutation
function registerMutation (store, type, handler, local) {
var entry = store._mutations[type] || (store._mutations[type] = []);
entry.push(function wrappedMutationHandler (payload) {
handler.call(store, local.state, payload);
});
}
//注册getter
function registerGetter (store, type, rawGetter, local) {
if (store._wrappedGetters[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
)
};
}
最终执行完actions,mutations和getters的注册,store对象的数据结构变成了如下形式.
store = {
_actions:{
home/getList: [ƒ], //home模块下actions中定义的getList函数
home/getNav: [ƒ], // home模块下actions中定义的getNav函数
getUser: [ƒ] //根模块下actions中定义的getUser函数
},
_mutations:{
logout: [ƒ], // 根模块下mutations中定义的logout函数
home/setList: [ƒ], // home模块下mutations中定义的setList函数
home/setNav: [ƒ] // home模块下mutations中定义的setNav函数
},
_wrappedGetters:{
home/getFilterList: ƒ wrappedGetter(store) // home模块下getters中定义的函数
}
}
上面花了这么大的力气就是为了将store的数据结构改造成这种形式,这种形式到底能发挥出什么作用呢?
熟悉vuex的同学都知道,调用actions函数要使用dispatch,调用mutations函数要使用commit.我们现在来看一下store对象下的dispatch和commit如何定义.
在页面组件内,当我们使用this.$store.dispatch(type,payload)就能触发某个action函数的执行.从调用方式来看,通常dispatch会传递两个参数,第一个是action的函数名,第二个是参数.commit的执行方式也类似.
先看dispatch的源码(代码如下),调用dispatch时通常会传入type和payload.Store.prototype.dispatch里面有句关键代码var entry = this._actions[type].通过type去store对象下的_actions寻找处理函数,再将payload传入执行该函数.
看到这里就已经明白了为什么store下面要构建_actions,_mutations和_wrappedGetters数据结构.
那是因为dispatch和commit它们要做的就是去_actions,_mutations寻找处理函数并执行.比如dispatch('getUser')就是执行根模块下的getUser.如果type为home/getList,那么dispatch执行就是home模块下的getList.
dispatch成为了统一的调用入口,而只要改变type的参数形式,就能让dispatch调用到任意模块下的action,如此便实现了dispatch函数对模块化的支持.commit实现原理类似,只不过它是从_mutations里面获取处理函数.
var Store = function Store (options) {
...
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)
};
...
}
Store.prototype.dispatch = function dispatch (_type, _payload) {
var this$1 = this;
// check object-style dispatch
var ref = unifyObjectStyle(_type, _payload);
var type = ref.type;
var payload = ref.payload;
var action = { type: type, payload: payload };
var entry = this._actions[type];
var result = entry.length > 1
? Promise.all(entry.map(function (handler) { return handler(payload); }))
: entry[0](payload);
return result.then(function (res) {
return res
})
};
Store.prototype.commit = function commit (_type, _payload, _options) {
var this$1 = this;
var ref = unifyObjectStyle(_type, _payload, _options);
var type = ref.type;
var payload = ref.payload;
var options = ref.options;
var mutation = { type: type, payload: payload };
var entry = this._mutations[type];
this._withCommit(function () {
entry.forEach(function commitIterator (handler) {
handler(payload);
});
});
};
现在我们来梳理一下整个执行流程,页面组件首先调用this.$store.dispatch("home/getList"),此时Store.prototype.dispatch函数就会响应从store._actions下面拿到home/getList对应的处理函数.
而这个处理函数是在registerAction函数里面生成的,对应着下面代码中的wrappedActionHandler.这个函数里面的handler正是开发者定义的actions里的函数,执行handler时修改它的上下文环境并传入该模块下的local对象里的属性作为参数.
handler是开发者定义的actions里的函数,函数内业务逻辑执行完毕后通常会执行commit操作调用mutation.函数内的commit正是下面的local.commit(后面会将),local.commit底层调用是全局store对象的commit,它又会根据type去store._mutations下面寻找处理函数.
wrappedActionHandler (payload, cb) {
var 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);
}
return res
});
store._mutations对应的处理函数上面已经讲过,它是在registerMutation函数内生成的.对应着下面代码中的wrappedMutationHandler函数.函数内的handler同样对应着开发者定义的mutations里的函数,将local.state作为参数传入其中执行.
function registerMutation (store, type, handler, local) {
var entry = store._mutations[type] || (store._mutations[type] = []);
entry.push(function wrappedMutationHandler (payload) {
handler.call(store, local.state, payload);
});
}
一般mutations函数里面会直接操作state,一旦改变了state的数据,页面就会重现渲染.整个流程走下来,我们便理解了actions和mutations只是一种改变数据的机制.
开发者一般会在actions里面定义异步逻辑获取数据,在mutations里面定义同步逻辑操作数据,这样机制能让修改数据的整个过程容易记录和追踪,从而增强了程序的健壮性.
action写异步,mutation写同步
现在回到文章的开头,为什么action里面建议写异步操作,而mutation定义成同步?
我们再看一下dispatch和commit的定义.在dispatch函数里面,通过this._actions[type]拿到了处理函数数组entry,接下来通过Promise.all执行数组内的handler,将返回值赋予result.
在上面讲到的生成action函数的wrappedActionHandler里面是将返回值Promise化的,换句话说entry里面的handler的返回值一定是一个Promise,所以这边能使用Promise.all来调用,返回的result仍然是一个Promise,result再调用then返回res.
Store.prototype.dispatch = function dispatch (_type, _payload) {
var this$1 = this;
var ref = unifyObjectStyle(_type, _payload);
var type = ref.type;
var payload = ref.payload;
var action = { type: type, payload: payload };
var entry = this._actions[type];
var result = entry.length > 1
? Promise.all(entry.map(function (handler) { return handler(payload); }))
: entry[0](payload);
return result.then(function (res) {
return res
})
};
Store.prototype.commit = function commit (_type, _payload, _options) {
var this$1 = this;
var ref = unifyObjectStyle(_type, _payload, _options);
var type = ref.type;
var payload = ref.payload;
var options = ref.options;
var mutation = { type: type, payload: payload };
var entry = this._mutations[type];
this._withCommit(function () {
entry.forEach(function commitIterator (handler) {
handler(payload);
});
});
};
由此可见上面dispatch最终返回的是一个Promise,那么在页面上(代码如下)调用dispatch就可以使用then来监听action函数的异步操作有没有执行完毕(代码如下).正是因为dispatch里面多了一层Promise处理,所以让异步操作变得容易监听和追踪,这就十分利于其他调试工具的记录.
我们再观察上面的commit,它内部直接获取handler函数就执行了.假如我们在mutations函数里面定义异步操作来改变state会怎样呢?
从源码角度来看,定义异步操作完全没有问题,最终修改state里面的数据也会让页面重新渲染.但是由于它内部没有像dispatch里面的Promise化处理,致使调试工具无法正确记录异步操作引起的状态变化,这样不利于开发环节对程序整个运行流程的把控和错误的追踪,因此官方不建议在mutations函数里面写异步操作.
//页面
this.$store.dispatch("getList").then(()=>{
// action异步操作完毕了
})
//vuex里面actions的定义
{
actions:{
getList({state,commit}){
//这里要返回一个Promise页面那边才监听的到异步操作是否完成
return new Promise((resolve)=>{
ajax(...).then((res)=>{
commit(res);
resolve(res);
})
})
}
}
}
到目前为止,已经将dispatch,commit,actions,mutations如何协作修改数据的流程介绍完毕了,但在这个环节当中,local对象里面四个属性的实现没有展开讲,现在来看一下它的实现原理.
local实现
local对象是运行makeLocalContext(store, namespace, path)返回的结果,它最终会被存储在module对象下面的context属性里.
local对象对外暴露了四个属性分别是dispatch,commit,getters和state.这四个属性只服务于当前模块(代码如下).
先看local中dispatch的实现,它首先判断noNamespace是否存在,如果noNamespace不存在说明当前的模块是根模块,直接调用全局store对象的dispatch方法.
如果存在说明当前是一个子模块,那么此时local的dispatch对应的是一个函数,该函数的参数也包含type和payload.
但是我们会发现它里面并没有实现dispatch的细节,函数内部仅仅只是将type和namespace做了字符串拼接,最终调用的还是全局的store.dispatch方法(commit处理和dispatch类似,不再赘述).
//store是使用new Vuex.Store({...})最终返回的实例对象
function makeLocalContext (store, namespace, path) {
var noNamespace = namespace === '';
var local = {
dispatch: noNamespace ? store.dispatch : function (_type, _payload, _options) {
var args = unifyObjectStyle(_type, _payload, _options);
var payload = args.payload;
var options = args.options;
var type = args.type;
if (!options || !options.root) {
type = namespace + type;
}
return store.dispatch(type, payload)
},
commit: noNamespace ? store.commit : function (_type, _payload, _options) {
var args = unifyObjectStyle(_type, _payload, _options);
var payload = args.payload;
var options = args.options;
var type = args.type;
if (!options || !options.root) {
type = namespace + type;
}
store.commit(type, payload, options);
}
};
Object.defineProperties(local, {
getters: {
get: noNamespace
? function () { return store.getters; }
: function () { return makeLocalGetters(store, namespace); }
},
state: {
get: function () { return getNestedState(store.state, path); }
}
});
return local
}
local对象里的dispatch和commit仅仅只是对type做了一下拼接,最终调用的还是全局store下的dispatch和commit.
现在再看getters和state.模块下的state是通过调用getNestedState(store.state, path)返回相应的值(代码如下).
//获取子级的state,那这个path对应着模块名称组合的数组
function getNestedState (state, path) {
return path.length
? path.reduce(function (state, key) { return state[key]; }, state)
: state
}
store.state是全局store下定义的根模块的状态(后面会介绍),通过path数组可以从最上层的根模块往下寻找,直到找到子模块返回.如果path是空数组,直接返回根模块的状态.
local下的getters只会触发该模块定义的getters函数(代码如下).它的实现原理是直接遍历全局store对象下getters对象定义的所有getter函数(store.getters的实现后面介绍),然后只取出属于当前模块的getters函数的函数名,再设置一层代理通过函数名去调用store.getters对象的函数,store.getters底层是从store._wrappedGetters获取处理函数的.
function makeLocalContext(){
...
Object.defineProperties(local, {
getters: {
get: noNamespace
? function () { return store.getters; }
: function () { return makeLocalGetters(store, namespace); }
}
});
return local;
}
// 假设获取子模块home的getters函数getList,最终被转化成获取根模块的getters的"home/getList"
function makeLocalGetters (store, namespace) {
var gettersProxy = {};
var splitPos = namespace.length;
Object.keys(store.getters).forEach(function (type) {
if (type.slice(0, splitPos) !== namespace) { return }
var localType = type.slice(splitPos);
Object.defineProperty(gettersProxy, localType, {
get: function () { return store.getters[type]; },
enumerable: true
});
});
return gettersProxy
}
到目前为止installModule安装模块的流程执行结束了,installModule执行完后store对象的数据结构变成了如下的样子,下一步执行resetStoreVM(this, state)完成数据的响应化处理.
{
...,
_actions:{
home/getList: [ƒ], //home模块下actions中定义的getList函数
home/getNav: [ƒ], // home模块下actions中定义的getNav函数
getUser: [ƒ] //根模块下actions中定义的getUser函数
},
_mutations:{
logout: [ƒ], // 根模块下mutations中定义的logout函数
home/setList: [ƒ], // home模块下mutations中定义的setList函数
home/setNav: [ƒ] // home模块下mutations中定义的setNav函数
},
_wrappedGetters:{
home/getFilterList: ƒ wrappedGetter(store) // home模块下getters中定义的函数
},
_modules:{
root:{
context: {dispatch: ƒ, commit: ƒ, getter,state},//根模块的local对象
state:{
home: {list:[]} // home模块下的 list
userInfo: {} // 根模块下的 userInfo
},
_children:
{
home: {
context: {dispatch: ƒ, commit: ƒ,getter,state}, //home模块的local对象
state: {list:[]},
_children: {},
_rawModule: {...} //原始配置
}
},
_rawModule: {...}, //原始配置
}
}
}
状态响应化
var Store = function Store (options) {
...
this._modules = new ModuleCollection(options);
// 获取根模块的状态
var state = this._modules.root.state;
//安装模块
installModule(this, state, [], this._modules.root);
//响应化处理
resetStoreVM(this, state);
...
}
在讲解installModule过程可知,根模块root对应的state是一个包含了父子级的层级结构,每一级都拥有自己的state状态(代码如下).
store._module = {
root:{
state:{
user_info:null,//user_info是根模块的状态信息
home:{ //list是home模块(子模块)的状态,以子模块的名称为``key``,状态为值赋予父模块的状态对象上
list:[]
}
}
}
}
现在将根模块的状态state传入resetStoreVM函数做响应化处理.我们在上面讲local里面getters实现的时候,发现它最终调用的是store.getters,下面代码里便包含了store.getters的定义.
store._wrappedGetters里面存储着所有模块定义的getters处理函数,它首先遍历wrappedGetters对象,获取函数名key和处理函数fn.然后将key和fn重新组合成函数放入computed对象中.
resetStoreVM执行响应化最关键的代码是重新构建了Vue实例并赋值给了store._vm,同时将根模块状态state作为初始值赋予了$$state,computed作为计算属性也赋值给了_vm.
store.getters通过Object.defineProperty重新设置get方法,因此从store.getters获取getters转向了从computed中获取,继而转向了从store._wrappedGetters中拿处理函数.
function resetStoreVM (store, state, hot) {
...
store.getters = {};
var wrappedGetters = store._wrappedGetters;
var computed = {};
forEachValue(wrappedGetters, function (fn, key) {
computed[key] = function () {
return fn(store);
}
Object.defineProperty(store.getters, key, {
get: function () { return store._vm[key]; },
enumerable: true // for local getters
});
});
store._vm = new Vue({
data: {
$$state: state
},
computed: computed
});
...
}
目前为止,store对象下的getters、dispatch以及commit的实现都介绍完毕,但还有一个最重要的store.state没有提及.
源码里面是通过下面的方式来定义store.state,它直接屏蔽了set修改值的方式,所以直接修改store的状态就会发出警告.
而get函数则被代理指向了this._vm._data.$$state.从这里可以看出store的数据状态是存放在另外一个Vue实例的$$state中的,因此store.state便具有了响应式特性.
var prototypeAccessors$1 = { state: { configurable: true } };
prototypeAccessors$1.state.get = function () {
return this._vm._data.$$state
};
prototypeAccessors$1.state.set = function (v) {
if (process.env.NODE_ENV !== 'production') {
assert(false, "use store.replaceState() to explicit replace store state.");
}
};
Object.defineProperties( Store.prototype, prototypeAccessors$1 );
store对象分发
Vue.use(Vuex)触发applyMixin执行安装操作(代码如下).applyMixin函数里面主要添加了一个mixin让每一个新创建的vue实例都会去执行vuexInit函数.
vuexInit函数执行完毕后,所有Vue实例都在自己的作用域内创建了一个$store变量,这些$store都指向了从顶层初始化根实例时传入的store对象.
function applyMixin (Vue) {
var version = Number(Vue.version.split('.')[0]);
if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit });
}
function vuexInit () {
var 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;
}
}
}
API实现
mapState
我们先看mapState在页面上有几种常用的调用方式.
computed:{
...mapState("home",{ //获取home模块下的list
list:"list"
})
}
...
computed:{
...mapState({
list:state=>state.home.list //获取home模块下的list
})
}
...
computed:{
...mapState(["login_info"]) //获取根模块下的login_info
}
通过上面调用方式推测可知,mapState执行最终返回一个对象,对象里面key为字符串,值为一个函数,用...解构放入computed中.
normalizeNamespace主要对namespace做了一下处理,如果采用上面第一种调用方式,传递了namespace为home,那么normalizeNamespace处理后的包裹的函数里,namespace变成了home/.
下面代码里res正是mapState最终返回的对象,key对应着计算属性调用的key,value为另外的一层封装函数mappedState.
mappedState函数首先会判断有没有发送模块名称namespace,如果namespace存在,就从子模块对象中通过context属性拿到该模块的local对象,那么从local对象取出来的state和getters只属于该模块.从而返回的结果也是从子模块中取出来的值.倘若namespace不存在,就直接从根模块下的state取值.
var mapState = normalizeNamespace(function (namespace, states) {
var res = {};
normalizeMap(states).forEach(function (ref) {
var key = ref.key;
//上面案例中,val根据调用方式不同,可能为``list``字符串,也可能是...mapState({list:fn})中的函数fn
var val = ref.val;
res[key] = function mappedState () {
var state = this.$store.state;
var getters = this.$store.getters;
if (namespace) {
var 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
});
mapGetter
mapGetter调用方式和mapState类似,也是在页面组件的computed中解构使用.
mapGetters返回一个对象res,对象的key对应着计算属性调用的key,value为另外的一层封装函数mappedGetter.
mappedGetter函数执行的结果指向全局store对象下定义的getters.
var mapGetters = normalizeNamespace(function (namespace, getters) {
var res = {};
normalizeMap(getters).forEach(function (ref) {
var key = ref.key;
var val = ref.val;
val = namespace + val;
res[key] = function mappedGetter () {
if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
return
}
return this.$store.getters[val]
};
});
return res
});
mapAction 和 mapMutations
mapActions通常使用的调用方式如下代码,将根模块下定义的actions里的函数getUser提取到组件methods中调用.
methods:{
...mapActions(['getUser'])
}
mapActions调用时被解构在Vue组件的methods里,因此可以推测出mapActions函数返回值也是一个对象.
观察下面代码.在使用上面的调用方式过程中,ref的key和val都为字符串getUser.res是mapActions执行完毕后最终返回的对象,其中以ref.key作为键,mappedAction函数作为值.
mappedAction函数首先会判断有没有传递模块名称namespace,如果namespace不存在就从全局store对象下面获取dispatch,否则就从子模块下的local中获取dispatch.
dispatch执行时传入val和参数args,这就相当于模拟调用dispatch(type,payload)的操作来触发actions函数执行.
mapMutation的代码和mapAction类似,可按照相同思路分析.
var mapActions = normalizeNamespace(function (namespace, actions) {
var res = {};
normalizeMap(actions).forEach(function (ref) {
var key = ref.key;
var val = ref.val; // actions里面定义的函数名
res[key] = function mappedAction () {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
var dispatch = this.$store.dispatch;
if (namespace) {
var module = getModuleByNamespace(this.$store, 'mapActions', namespace);
if (!module) {
return
}
dispatch = module.context.dispatch;
}
return typeof val === 'function'
? val.apply(this, [dispatch].concat(args))
: dispatch.apply(this.$store, [val].concat(args))
};
});
return res
});
尾言
本文从数据的模块化处理,响应式改造以及常用API的实现来探索了vuex实现的整体脉络.
从上面篇幅可以看出来,vuex里面大部分处理逻辑都是为了支持复杂的多模块数据结构.如果没有多模块的引入,它里面的响应式设计是非常容易实现的.