导语
随着应用复杂度的增加,我们需要考虑如何进行应用的状态管理,将业务逻辑与界面交互相剥离,Vue 为我们提供了方便的组件内状态管理的机制Vuex。本文将与实际场景应用相结合,分析我们在调用Vuex API,Vuex源码内是如何实现的。
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
1.Vuex核心思想
Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。
Vuex 和单纯的全局对象有以下两点不同:
Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
-
你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是提交 (commit) mutation。
通过下面demo,我将从Store注册、数据修改流程来介绍Vuex里是如何实现的。
const ModuleA = {
namespaced: true,
state:{
count: 1,
},
mutations:{
increment(state){
state.count++;
}
},
actions:{
increment(context){
context.commit('increment');
}
},
getters:{
getNewCount(state){
return state.count + 1
}
}
}
const ModuleB = {
namespaced: true,
state:{
count: 1,
},
mutations:{
increment(state){
state.count++;
}
},
actions:{
increment(context){
context.commit('increment');
}
},
getters:{
getNewCount(state){
return state.count + 1
}
}
}
const store = new Vuex.Store({
//定义每个module子仓库
modules: {
moduleA: ModuleA,
moduleB: ModuleB
}
});
2. Store初始化过程
通常在使用Vuex时,会去调用new Vuex.Store方法,当前方法实现整个Store注册过程。Store主要执行分为ModuleCollection,installModule、resetStoreVM、注册插件步骤。
ModuleCollection主要功能将所有的modules建立父子关系,通过整个register逻辑,把整个关系建立起来,形成module树状结构。
ModuleCollection最终返回modules对象,如图:
installModule 方法对每个state、mutation、action、getter做了一层namespace。
上述在代码中,我们定义ModuleA、ModuleB actions方法、state都是相同的,前面并没有加前缀,installModule方法就是在当前的加上namespace。这样的好处,互补影响。
makeLocalContext方法,遍历所有getters,然后给当前的getter加上namespace。这样就实现了在模块内调用getter里,通过defineProperty store.getters[type]就拿到了当前namespace的getter,这里的type就是拼接后的getter方法。makeLocalContext最终返回local对象,这个local在遍历Mutation、Action、Getter时使用。
mutation实现,主要就是判断当前module有没有定义mutations,如果定义,循环mutations里的方法。通过namespace+key拼接,然后通过registerMutation方法实现注册。registerMutation会去把当前的方法push一个数组,最终返回wrappedMutationHandler方法,这个方法在我们执行commit方法的时候,就会执行handler.call方法,并最终返回当前local state。这就实现了我们在mutation方法中使用的state,实际上就是当前模块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);
});
}
action方法实现和mutation是类似的,action方法里会判断当前的action是不是root属性,如果是root就不做namespace拼接,然后在去执行registerAction方法,这里和registerMutation不同的是,他返回的对象里有当前模块的dispatch、commit、getters、state和root getters、root state。最后判断当前返回值有没有Promise,如果没有Promise,这里会把实际的返回转成Promise化。
function registerAction (store, type, handler, local) {
var entry = store._actions[type] || (store._actions[type] = []);
entry.push(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);
}
});
}
getter方法实现也是循环遍历当前模块的getter,然后做了一层namespace拼接,最后调用registerGetter方法。当我们调用getters里的方法就会调用wrappedGetter这个方法,并把当前模块的store传入,最终返回rawGetter方法,方法返回4个参数第一个参数当前模块state、第二个当前模块getters、第三个root state、第四个root getters。这个和上述方法不太一样,这里返回是参数不是对象。rawGetter方法实际上就是当前getters内定义的方法。
function registerGetter (store, type, rawGetter, local) {
store._wrappedGetters[type] = function wrappedGetter (store) {
return rawGetter(
local.state, // local state
local.getters, // local getters
store.state, // root state
store.getters // root getters
)
};
}
installModule除了注册上述几个主要的步骤,他还做了针对children操作,判断当前的模块有没有children,如果有child再去调用installModule执行模块注册。
上图当执行完installModule方法后,store拿到的对象。
resetStoreVM是主要做什么的?
Vuex store提供在外层可以去调用state、getters方法,resetStoreVM就是实现在外层可以去调用state、getters功能;
store.state.count;
store.getters.getCount
plugin
plugin判断当前模块有没有传入plugins数组,如果有的话遍历plugins数组,然后调用plugin方法。
3. 数据修改过程
Vuex 本质上数据都存储在state下面,state修改只能通过mutations、actions去修改。actions与mutations的区别是,actions会执行一些异步操作,通常这种方式用于数据请求。下面介绍第一种commit方式:
store.commit('increment')
下面接下commit主要做了什么事情?
Store.prototype.commit = function commit (_type, _payload, _options) {
var this$1 = this;
// check object-style commit
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);
});
});
};
通过commit传入参数,拿到type。然后通过this._mutations[type]拿到wrappedMutationHandler方法,this._mututationsz在初始化state中有介绍过,然后在执行registerMutations里的handler方法,调用实际执行的mutation方法。
第二种dispatch方式:
store.dispatch('increment')
对于dispatch是怎么实现的?
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);
};
dispatch逻辑和commit逻辑比较相似,他也是通过this._actions[type]拿到当前action执行的方法,this._action在初始化state中有说。然后判断当前的entry有几个回调方法,如果回调方法大于1的话,执行Promise.all里所有方法;如果回调里方法个数小于等于1,就直接调用该方法。
4.组件绑定的辅助函数
Store本身而言就是一个原生对象,他构造了一个数据仓库,他提供了一些常用的api修改这些数据仓库内容。在我们组件中也是可以同过store.state实现,但是这种方式我们使用组件中使用并不是太爽,所以vuex在这基础上又提供一些其他好用语法糖。
import { mapState, mapGetters } from 'vuex'
export default {
name: 'App',
computed:{
...mapState('moduleA',{
beforeCount: 'count'
}),
...mapGetters('moduleA',{
afterCount: 'getCount'
})
}
}
在这里我们并没有通过store去读取state,而是通过mapState、mapGetters实现的读取state,先来分析mapState实现
mapState实现
var mapState = normalizeNamespace(function (namespace, states) {
var res = {};
normalizeMap(states).forEach(function (ref) {
var key = ref.key;
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
});
mapSate支持两个参数,namespace和states两个参数。normalizeMap方法对传入states进行key、value转换,这样做的好处就是每个key都对应独立的返回。mappedState方法主要工作通过传入namespace调用getModuleByNameSpace拿到当前模块,然后返回对应模块的state。
mapGetter实现
mapGetter和mapState很像,首先对namespace进行一个拼接,拼接完之后,通过getModuleByNamespace查找,如果有的话,直接执行对应模块store.getters;如果没有,直接return。
接下来介绍另外两个对于数据存储和修改方法。
import {mapState, mapGetters, mapMutations, mapActions} from 'vuex'
export default {
name: 'App',
methods:{
...mapActions('moduleA',{
getMapActions: 'increment'
}),
...mapMutations('moduleA',{
getMapMutations: 'increment'
})
}
}
mapMutation实现
首先对当前传入mutations传做一层循环遍历,最终返回res[key]对象,在执行循环过程中调用getModuleByNamespace查找对应模块,如果没有,直接return。如果有的话,执行对应模块下的commit方法,最后判断mutations里方法是一个val还是function。并执行对应方法。
var mapActions = normalizeNamespace(function (namespace, actions) {
var res = {};
normalizeMap(actions).forEach(function (ref) {
var key = ref.key;
var val = ref.val;
res[key] = function mappedAction () {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
// get dispatch function from store
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
});
mapAction实现
对于actions和mutation方法是类似的,只是区别在于这里执行的是dispatch方法。
辅助函数是在原有的基础上,实现以最少的代码实现原有的方法,简化开发者使用。
5.动态更新模块
Vuex除了提供辅助函数,还提供动态加载新模块。调用方法如下:
this.$store.registerModule('moduleC', {
namespaced: true,
state: {
count: 1,
},
mutations:{
increment(state){
state.count++;
}
},
actions:{
increment(context){
context.commit('increment');
}
},
getters:{
getCount(state){
return state.count + 1
}
}
})
最终我们拿到的store,如下:
上述的代码在Vuex主要实现:
function registerModule (path, rawModule, options) {
if ( options === void 0 ) options = {};
if (typeof path === 'string') { path = [path]; }
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);
};
registerModule检测当前的path是不是string,检测完了之后,就会调用当前module里的register方法,然后再调用installModule、resetStoreVM实现新的module注册。installModule、resetStoreVM在初始化state中介绍,这里就不做解释。
Vuex提供注册方法,就有注销方法——unregisterModule方法。
function unregisterModule (path) {
var this$1 = this;
if (typeof path === 'string') { path = [path]; }
this._modules.unregister(path);
this._withCommit(function () {
var parentState = getNestedState(this$1.state, path.slice(0, -1));
Vue.delete(parentState, path[path.length - 1]);
});
resetStore(this);
};
unregisterModule方法,通过传入path,执行this.modules的unregister方法,然后最终会把数据做一次恢复。最后再执行resetStore。resetStore方法会对actions、mutations方法做一次清空。清空完之后,再重新执行一次installModule、resetStoreModule方法。
6.总结
通常我们开发时通过传参的方法对于多层嵌套的组件将会非常麻烦,Vuex解决了组件之间共享同一状态的麻烦问题。其实Vuex还提供一些额外辅助方法,尤其是mapXXX的设计,使程序更加简洁,有更高的可读性。我们在使用API的时候更加方便。这也是我们今后在设计一些JS库的时候,从API的角度中应该学习的方向