前言
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
里面大部分处理逻辑都是为了支持复杂的多模块数据结构.如果没有多模块的引入,它里面的响应式设计是非常容易实现的.