彻底搞懂vue之vuex原理篇

你真的懂vuex吗?先抛出几个问题?命名空间的原理?辅助函数的原理?插件用法是否了解?为什么每个组件都能访问到 s t o r e 访 t h i s . store这个实例属性?为什么访问this. store.getters[‘a/xx’]而不是this.KaTeX parse error: Unexpected character: '�' at position 161: …tate?ok,答案都在下面噢�̲�注册插件这个就不用多说了,注…storefunction applyMixin (Vue) {
var version = Number(Vue.version.split(’.’)[0]);
//判断版本号
if (version >= 2) {
//使用mixin混入beforeCreate生命周期
Vue.mixin({ beforeCreate: vuexInit });
} else {
var _init = Vue.prototype._init;
Vue.prototype._init = function (options) {
if ( options === void 0 ) options = {};

  options.init = options.init
    ? [vuexInit].concat(options.init)
    : vuexInit;
  _init.call(this, options);
};

}

//使每个组件都能访问到KaTeX parse error: Expected '}', got 'EOF' at end of input: …options = this.options;
// store injection
if (options.store) {
this.KaTeX parse error: Expected 'EOF', got '}' at position 100: …ons.store; }̲ else if (optio…store) {
this. s t o r e = o p t i o n s . p a r e n t . store = options.parent. store;
}
}
}复制代码store构造函数class Store{
constructor (options = {}) {
assert(Vue, must call Vue.use(Vuex) before creating a store instance.)
assert(typeof Promise !== ‘undefined’, vuex requires a Promise polyfill in this browser.)
const {
plugins = [],
strict = false
} = options

let {
  state = {}
} = options
if (typeof state === 'function') {
  state = state()
}

//用来记录提交状态,主要用来检查是否正在进行commit操作  
this._committing = false
//存储actions
this._actions = Object.create(null)
//存储mutations
this._mutations = Object.create(null)
//存储getters
this._wrappedGetters = Object.create(null)
//存储modules
this._modules = new ModuleCollection(options)
//存储module和其namespace的对应关系。
this._modulesNamespaceMap = Object.create(null)
//订阅监听
this._subscribers = []
//监听器
this._watcherVM = new Vue()

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)
}
this.strict = strict
installModule(this, state, [], this._modules.root)
resetStoreVM(this, state)
plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))

}
}复制代码1. 主要片段1const { 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)
}复制代码上面这段代码是啥意思呢?主要解决的问题是调用commit的时候this的指向问题,比如下面这段代码actions:{
addSalaryAction({commit},payload){
setTimeout(()=>{
commit(‘addSalary’,payload)
},1000)
}
}复制代码调用commit的时候,只是执行commit这个方法,this指向的并不是store实例,当然解决方案有很多,源码做了科里化,这样执行commit的时候都会返回一个用store实例调用的结果commit
=>
function boundCommit (type, payload, options) {
return commit.call(store, type, payload, options)
}复制代码经典!!!nice!!!2. 插件管理没错store内部也是有插件可以定制的用法如下:import MyPlugin=()=>store=>{
store.subscribe(xx),
store.watch(xx)
}
const store = new Vuex.Store({
plugins: [MyPlugin()]
}复制代码源码很简单://查看是否传入plugins
var plugins = options.plugins; if ( plugins === void 0 ) plugins = [];
//遍历plugins传入store实例作为参数
plugins.forEach(function (plugin) { return plugin(this$1); });复制代码3.resetStoreVMfunction resetStoreVM (store, state, hot) {
var oldVm = store._vm;//在store中定义的vue实例

// 创建getters
store.getters = {};
// 重置缓存
store._makeLocalGettersCache = Object.create(null);
var wrappedGetters = store._wrappedGetters;
var computed = {};
// 为getters设置代理 4.2会着重讲到
forEachValue(wrappedGetters, function (fn, key) {
computed[key] = partial(fn, store);
Object.defineProperty(store.getters, key, {
get: function () { return store._vm[key]; },
enumerable: true // 可枚举
});
});

var silent = Vue.config.silent;
Vue.config.silent = true;
store._vm = new Vue({
data: {
$$state: state
},
computed: computed
});
Vue.config.silent = silent;

// 严格模式只能通过commit修改state
if (store.strict) {
enableStrictMode(store);
}
//是否有旧的实例
if (oldVm) {
if (hot) {
store._withCommit(function () {
oldVm._data.KaTeX parse error: Expected 'EOF', got '}' at position 21: … = null; }̲); } Vu…state }, function () {
if (process.env.NODE_ENV !== ‘production’) {
//通过实例属性_commiting来判断,只要不经过commit,_commiting就为false,就会报错
assert(store._committing, “do not mutate vuex store state outside mutation handlers.”);
}
}, { deep: true, sync: true });
}复制代码_withCommit做实例属性_commiting状态的改变,主要用来在严格模式下确保只能通过commit修改state,因为只要通过commit修改_commiting属性就会发生改变Store.prototype._withCommit = function _withCommit (fn) {
var committing = this._committing;
this._committing = true;
fn();
this._committing = committing;
};复制代码4. 主要API1.state的原理如何通过访问this. s t o r e . s t a t e . x x 访 s t a t e s t a t e v u e s t a t e d a t a c l a s s S t o r e c o n s t r u c t o r ( o p t i o n s ) t h i s . v m = n e w V u e ( d a t a ( ) r e t u r n s t a t e : o p t i o n s . s t a t e ) g e t s t a t e ( ) r e t u r n t h i s . v m . s t a t e / / s t a t e s e t s t a t e ( v ) t h r o w n e w E r r o r ( ) 2. g e t t e r s ? 访 t h i s . store.state.xx就能访问对应state的值?并且改变state后视图也能对应渲染很简单,创建一个vue实例,state作为data中的数据即可class Store { constructor(options) { this._vm = new Vue({ data() { return { state: options.state } } }) } get state(){ return this._vm.state } //直接设置state会报错 set state(v){ throw new Error('') } }复制代码2.getters?如何通过访问this. store.getters.a就能返回对应方法执行后的返回值?也就是this. s t o r e . g e t t e r s . a = > r e t u r n t h i s . store.getters.a => return this. store.getters.a(state)很简单做一层代理即可Object.defineProperty(store.getters, key, {
get: function () { return store._vm[key]; },
enumerable: true // for local getters
});复制代码这里最重要一点也是面试经常会问到的修改state之后getters的视图数据如何动态渲染?源码中使用的方法是将其放到vue实例的computed中var wrappedGetters = store._wrappedGetters;
//拿到存储的所有getters
var computed = {};
//遍历getters
forEachValue(wrappedGetters, function (fn, key) {
//存储到computed对象中
computed[key] = partial(fn, store);//partical的作用是将其变成()=>{fn(store)}
//设置getters的代理,访问getters就是访问computed
Object.defineProperty(store.getters, key, {
get: function () { return store._vm[key]; },
enumerable: true
});
});

store._vm = new Vue({
data: {
$$state: state
},
//赋值给计算属性
computed: computed
});复制代码wrappedGetters是安装的所有getters的值,需要在installModule中看,下面会提到3.commit提交mutationscommit是store的实例方法,commit传入key值和参数来执行mutations中对应的方法几种不同的用法:commit({type:xx,payload:xx})commit(type, payload)我们看下源码:Store.prototype.commit = function commit (_type, _payload, _options) {
var this$1 = this;
//对传入的参数做统一处理,因为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];
//检查是否有对应的mutations
if (!entry) {
if (process.env.NODE_ENV !== ‘production’) {
console.error(("[vuex] unknown mutation type: " + type));
}
return
}
//改变commiting状态
this._withCommit(function () {
entry.forEach(function commitIterator (handler) {
handler(payload);
});
});
//发布所有订阅者
this._subscribers.forEach(function (sub) { return sub(mutation, this$1.state); });

if (
process.env.NODE_ENV !== ‘production’ &&
options && options.silent
) {
console.warn(
"[vuex] mutation type: " + type + ". Silent option has been removed. " +
‘Use the filter functionality in the vue-devtools’
);
}
};复制代码4.dispatch分发actions执行异步 actions: {
minusAgeAction({commit},payload){
console.log(this)
setTimeout(()=>{
commit(‘minusAge’,payload)
},1000)
},
},复制代码有一个注意点是actions中的commit中的this指向问题,上面已经讲过了,我们看下核心源码: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];
if (!entry) {
if (process.env.NODE_ENV !== ‘production’) {
console.error(("[vuex] unknown action type: " + type));
}
return
}

try {
this._actionSubscribers
.filter(function (sub) { return sub.before; })
.forEach(function (sub) { return sub.before(action, this$1.state); });
} catch (e) {
if (process.env.NODE_ENV !== ‘production’) {
console.warn("[vuex] error in before action subscribers: ");
console.error(e);
}
}

var result = entry.length > 1
? Promise.all(entry.map(function (handler) { return handler(payload); }))
: entry0;

return result.then(function (res) {
try {
this$1._actionSubscribers
.filter(function (sub) { return sub.after; })
.forEach(function (sub) { return sub.after(action, this$1.state); });
} catch (e) {
if (process.env.NODE_ENV !== ‘production’) {
console.warn("[vuex] error in after action subscribers: ");
console.error(e);
}
}
return res
})
};复制代码modules—vuex中是如何管理module的?最基本的用法:import Vue from ‘vue’
import Vuex from ‘vuex’
Vue.use(Vuex)

export default new Vuex.Store({
state: {
age: 10,
salary: 6500
},
getters: {
totalSalary(state) {
return state.salary * 12
}
},
modules: {
a: {
namespaced: true,
state: {
age: 100,
salary:12000
},
getters:{
totalSalary(state){
return state.salary*12
}
},
modules:{
b: {
namespaced:true,
state:{
age:200
}
}
}
}
}
})复制代码访问a中的state=>this. s t o r e . s t a t e . a . a g e 访 a g e t t e r s = > t h i s . store.state.a.age访问a中的getters=>this. store.getters[‘a/totalSalary’]访问a中子模块b中的state=>this. s t o r e . s t a t e . a . b . a g e 访 a b g e t t e r s = > t h i s . store.state.a.b.age访问a中子模块b中的getters=>this. store.getters[‘a/b/totalSalary’]1.ModuleCollection这个类的作用就是将传入的modules格式化便于管理,比如传入以下的modulesmodules:{
a: {
namespaced: true,
state: {
age: 100,
salary: 12000
},
getters: {
totalSalary(state) {
return state.salary * 12
}
},
modules: {
c: {
namespaced: true,
state: {
age: 300,
salary: 14000
},
getters: {
totalSalary(state) {
return state.salary * 12
}
},
}
}
},
b:{}
}复制代码=>格式化成root:{
_raw:rootModule,
_children:{
a:{
_raw:aModule,
_children:{
c:{
_raw:bModule,
state:cState
}
},
state:aState
},
b:{
_raw:bModule,
state:bState
}
},
state:xx
}复制代码实现起来很简单,但是这里有一个bug就是无法递归到第二层级,就是说下面的代码转化的a和c在同一层级,但很明显的是c是a的children?如何改进呢?小伙伴们可以先试着想一下class ModuleCollection{
constructor(options){
this.register([],options)
}
register(path,rootModule){
let newModule={
_rawModule:rootModule,
_children:{},
state:rootModule.state
}
if(path.length===0){
this.root=newModule
}else{
this.root._children[path[path.length-1]]=rootModule
}

    if(rootModule.modules){
        forEach(rootModule.modules,(key,value)=>{
            this.register(path.concat(key),value)
        })
    }
}

}复制代码改进过后:class ModuleCollection{
constructor(options){
this.register([],options)
}
register(path,rootModule){
let newModule={
_rawModule:rootModule,
_children:{},
state:rootModule.state
} //vuex源码是将其变成一个module类

    if(path.length===0){
        this.root=newModule
    }else{
        //第一次path为[a]
        //第二次path变成[a,c]=>所以c前面的必然是父级,但也可能存在多个层级及父亲的父亲(及如果是[a,c,d]的话那么意味着a是d的父亲的父亲),所以需要用reduce
        //然后又回到与a平级,于是path重新变成[b]
        let parent=path.slice(0,-1).reduce((prev,curr)=>{
            return prev._children[curr]
        },this.root) //vuex源码中这一段会封装成一个get实例方法
        
        parent._children[path[path.length-1]]=newModule //vuex源码这一段会作为module类的一个实例属性addChild,因为parent是一个module类所以可以调用addChild方法
    }
    if(rootModule.modules){
        forEach(rootModule.modules,(key,value)=>{
            this.register(path.concat(key),value)
        })
    }
}

}复制代码2.installModulemodules定义完毕,下一步就是如何在 s t o r e a s t a t e g e t t e r s m o d u l e store上取值了,比如想要调用a中的state或者getters?如何做呢,很简单我们需要将整个module安装到 Store上2.1 安装state在实现之前我们先看下用法,比如有我们注册了这样的moduleexport default new Vuex.Store({
state: {
age: 10,
salary: 6500
},
getters: {
totalSalary(state) {
return state.salary * 12
}
},
mutations: {
addAge(state, payload) {
state.age += payload
},
minusAge(state, payload) {
state.age -= payload
}
},
actions: {
minusAgeAction({ commit }, payload) {
setTimeout(() => {
commit(‘minusAge’, payload)
}, 1000)
}
},
modules: {
a: {
namespaced:true,
state: {
age: 100,
salary: 10000
},
getters: {
totalSalaryA(state) {
return state.salary * 12
}
},
mutations: {
addAge(state, payload) {
console.log(1)
state.age += payload
},
minusAge(state, payload) {
state.age -= payload
}
},
modules: {
c: {
namespaced: true,
state: {
age: 300,
salary: 14000
},
getters: {
totalSalaryC(state) {
return state.salary * 12
}
},
modules:{
d:{}
}
}
}
},
b:{}
}
})复制代码我们希望访问this. s t o r e . s t a t e . a . a g e a a g e c h i l d r e n s t a t e s t a t e s t a t e c o n s t i n s t a l l M o d u l e = ( s t o r e , s t a t e , p a t h , r o o t M o d u l e ) = > i f ( p a t h . l e n g t h > 0 ) l e t p a r e n t = p a t h . s l i c e ( 0 , 1 ) . r e d u c e ( ( p r e v , c u r r ) = > r e t u r n p r e v [ c u r r ] . s t a t e , s t a t e ) p a r e n t [ p a t h [ p a t h . l e n g t h 1 ] ] = r o o t M o d u l e . s t a t e f o r E a c h ( r o o t M o d u l e . r a w M o d u l e . m o d u l e s , ( k e y , v a l u e ) = > i n s t a l l M o d u l e ( s t o r e , s t a t e , p a t h . c o n c a t ( k e y ) , v a l u e ) ) p a t h p a t h s t o r e s t a t e v u e s t a t e s t a t e n v u e v u e s e t p a r e n t [ p a t h [ p a t h . l e n g t h 1 ] ] = r o o t M o d u l e . s t a t e = > V u e . s e t ( p a r e n t , p a t h [ p a t h . l e n g t h 1 ] , r o o t M o d u l e . s t a t e ) 2.2 g e t t e r s g e t t e r s 1. 2. a g e t t e r s t h i s . store.state.a.age就能拿到a模块下的age,原理其实和之前格式化的原理一样,格式化是需要递归然后把子模块放到父模块的_children中,而安装state则是将子模块中的state都放到父级的state中const installModule=(store,state,path,rootModule)=>{ if(path.length>0){ let parent=path.slice(0,-1).reduce((prev,curr)=>{ return prev[curr].state },state) parent[path[path.length-1]]=rootModule.state } forEach(rootModule._rawModule.modules,(key,value)=>{ installModule(store,state,path.concat(key),value) }) }复制代码如果不是很理解可以先打印path,看每一次path的取值就会明朗很多,这样就结束了吗?并没有那么简单,细心的小伙伴会发现我们store类中的state是响应式的,是基于vue的发布订阅模式,就是一旦state中值发生改变会触发试图的更新但是我们上述是在state中又增加了n个对象,这些对象并不是响应式的(如果了解vue的原理这一块不用多讲),意思就是后续增加的对象改变不会触发试图的更新,所以我们需要将这些后增加的也变成响应式的,很简单,vue中有静态方法set可以帮您轻松搞定parent[path[path.length-1]]=rootModule.state => Vue.set(parent,path[path.length-1],rootModule.state)复制代码2.2 安装gettersgetters的用法比较特殊1.是不能注册相同名称的方法2.如果没有注册命名空间,想获取a模块中的getters中的方法,用法为this. store.getters.属性名3.如果注册了命名空间,想获取a模块中的getters中的方法,用法为this. s t o r e . g e t t e r s [ a / x x ] c o n s t i n s t a l l M o d u l e = ( s t o r e , s t a t e , p a t h , r o o t M o d u l e ) = > l e t g e t t e r s = r o o t M o d u l e . r a w M o d u l e . g e t t e r s / / m o d u l e g e t t e r s i f ( g e t t e r s ) / / g e t t e r s s t o r e . g e t t e r s S t o r e f o r E a c h ( g e t t e r s , ( k e y , v a l u e ) = > O b j e c t . d e f i n e P r o p e r t y ( s t o r e . g e t t e r s , k e y , g e t : ( ) = > r e t u r n v a l u e ( r o o t M o d u l e . s t a t e ) ) ) / / m o d u l e f o r E a c h ( r o o t M o d u l e . r a w M o d u l e . m o d u l e s , ( k e y , v a l u e ) = > i n s t a l l M o d u l e ( s t o r e , s t a t e , p a t h . c o n c a t ( k e y ) , v a l u e ) ) 2.3 m u t a t i o n s m u t a t i o n s g e t t e r s 1. 2. a m u t a t i o n s t h i s . store.getters['a/xx']我们先看前两个如何实现?const installModule=(store,state,path,rootModule)=>{ let getters=rootModule._rawModule.getters //当前的module是否存在getters if(getters){ //如果存在把getters都挂载到store.getters上也就是Store类的实例上 forEach(getters,(key,value)=>{ Object.defineProperty(store.getters,key,{ get:()=>{ return value(rootModule.state) } }) }) } //递归module forEach(rootModule._rawModule.modules,(key,value)=>{ installModule(store,state,path.concat(key),value) }) }复制代码2.3 安装mutationsmutations的用法和getters又不同1.可以注册相同名称的方法,相同方法会存入到数组中按顺序执行2.如果没有注册命名空间,想获取a模块中的mutations中的方法,用法为this. store.commit(‘属性名’)3.如果注册了命名空间,想获取a模块中的mutations中的方法,用法为this. s t o r e . c o m m i t [ a / x x ] c o n s t i n s t a l l M o d u l e = ( s t o r e , s t a t e , p a t h , r o o t M o d u l e ) = > l e t m u t a t i o n s = r o o t M o d u l e . r a w M o d u l e . m u t a t i o n s / / m o d u l e m u t a t i o n s i f ( m u t a t i o n s ) / / m u t a t i o n s f o r E a c h ( m u t a t i o n s , ( k e y , v a l u e ) = > i f ( m u t a t i o n s ) f o r E a c h ( m u t a t i o n s , ( k e y , v a l u e ) = > s t o r e . m u t a t i o n s [ k e y ] = s t o r e . m u t a t i o n s [ k e y ] [ ] s t o r e . m u t a t i o n s [ k e y ] . p u s h ( p a y l o a d = > v a l u e ( r o o t M o d u l e . s t a t e , p a y l o a d ) ) ) ) / / m o d u l e f o r E a c h ( r o o t M o d u l e . r a w M o d u l e . m o d u l e s , ( k e y , v a l u e ) = > i n s t a l l M o d u l e ( s t o r e , s t a t e , p a t h . c o n c a t ( k e y ) , v a l u e ) ) 2.4 a c t i o n s a c t i o n s m u t a t i o n s 1. s t a t e s t a t e c o m m i t 2. t h i s . store.commit['a/xx']我们先看前两个如何实现?const installModule=(store,state,path,rootModule)=>{ let mutations=rootModule._rawModule.mutations //当前的module是否存在mutations if(mutations){ //如果存在,看当前的属性在mutations中是否已经存在,不存在的话先赋值为空数组,然后在将方法放入,出现同名也没关系,调用时依次执行 forEach(mutations,(key,value)=>{ if (mutations) { forEach(mutations, (key, value) => { store.mutations[key]=store.mutations[key]||[] store.mutations[key].push( payload => { value(rootModule.state, payload) }) }) } }) } //递归module forEach(rootModule._rawModule.modules,(key,value)=>{ installModule(store,state,path.concat(key),value) }) }复制代码2.4 安装actionsactions和mutations原理几乎一模一样,主要区别如下1.调用时接受的参数不是state而是一个经过处理的对象可以解构出state,commit等属性2.调用时没有命名空间是this. store.dispatch(xx),有命名空间是this. s t o r e . d i s p a t c h ( a / x x ) c o n s t i n s t a l l M o d u l e = ( s t o r e , s t a t e , p a t h , r o o t M o d u l e ) = > l e t a c t i o n s = r o o t M o d u l e . r a w M o d u l e . a c t i o n s / / m o d u l e a c t i o n s i f ( a c t i o n s ) / / a c t i o n s f o r E a c h ( a c t i o n s , ( k e y , v a l u e ) = > s t o r e . a c t i o n s [ k e y ] = s t o r e . a c t i o n s [ k e y ] [ ] s t o r e . a c t i o n s [ k e y ] . p u s h ( p a y l o a d = > v a l u e ( s t o r e , p a y l o a d ) ) ) / / m o d u l e f o r E a c h ( r o o t M o d u l e . r a w M o d u l e . m o d u l e s , ( k e y , v a l u e ) = > i n s t a l l M o d u l e ( s t o r e , s t a t e , p a t h . c o n c a t ( k e y ) , v a l u e ) ) 3. n a m e s p a c e 1. m u t a t i o n s a c t i o n s m u t a t i o n a c t i o n 2. m u t a t i o n s a c t i o n s a m u t a t i o n m u t a t i o n a A P I : a m u t a t i o n s a d d N u m t h i s . store.dispatch('a/xx')我们先看前两个如何实现?const installModule=(store,state,path,rootModule)=>{ let actions=rootModule._rawModule.actions //当前的module是否存在actions if(actions){ //如果存在,看当前的属性在actions中是否已经存在,不存在的话先赋值为空数组,然后在将方法放入,出现同名也没关系,调用时依次执行 forEach(actions,(key,value)=>{ store.actions[key]=store.actions[key]||[] store.actions[key].push( payload => { value(store, payload) } ) }) } //递归module forEach(rootModule._rawModule.modules,(key,value)=>{ installModule(store,state,path.concat(key),value) }) }复制代码3.命名空间也就是所谓的namespace,为什么需要命名空间?1.不同模块中有相同命名的mutations、actions时,不同模块对同一 mutation 或 action 作出响应2.辅助函数(下面会讲到)在上面安装模块的时候我们讲过mutations和actions中如果名称相同并不矛盾,会将其都存入到数组中依次执行,那这样带来的后果就是我只想执行a模块中的mutation但是没想到先执行了最外面定义的mutation,然后在执行a模块中的,所以我们需要引入命名空间加以区分(有人说那我起名字不一样不就行了吗?相信我项目大或者多人合作的话这一点真的很难保证)我们先看下命名空间的API:比如我们想要调用a模块中mutations的addNum方法,我们一般这样调用this. store.commit(‘a/addNum’),我们之前调用是
https://www.jianshu.com/p/65f42bd316bb
https://www.jianshu.com/p/205c601a5949
https://www.jianshu.com/p/3dee4e87a2da
https://www.jianshu.com/p/027d6c6540ac
this.$store.commit(‘addNum’)所以我们只需要在安装mutations时把对应属性前加一个模块名的前缀即可。整理步骤如下先判断该模块是否有命名空间,如果有的话需要进行拼接返回,比如a模块有命名空间返回a/,a模块下的c模块也有则返回a/c/将返回的命名空间放在mutations或者actions属性名的前面加以区分我们看下vuex内部源码ModuleCollection.prototype.getNamespace = function getNamespace (path) {
var module = this.root;
return path.reduce(function (namespace, key) {
module = module.getChild(key);
return namespace + (module.namespaced ? key + ‘/’ : ‘’)
}, ‘’)
};
//getNamespace是实例方法用来返回拼接后的命名空间

function installModule (store, rootState, path, module, hot) {
var isRoot = !path.length;
var namespace = store._modules.getNamespace(path);

if (module.namespaced) {
//如果有命名空间,则存放到_modulesNamespaceMap对象中
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;
}

if (!isRoot && !hot) {
var parentState = getNestedState(rootState, path.slice(0, -1));
var moduleName = path[path.length - 1];
store._withCommit(function () {
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);
});
}

var local = module.context = makeLocalContext(store, namespace, path);

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);
});

module.forEachChild(function (child, key) {
installModule(store, rootState, path.concat(key), child, hot);
});
}复制代码ok这样基本就实现了,但是actions似乎还有问题,我们举个例子actions: {
minusAgeAction({ commit }, payload) {
setTimeout(() => {
commit(‘minusAge’, payload) //并没有使用命名空间,如何精确调用b模块mutation?
}, 1000)
}
},复制代码假设上面是a模块的actions,我们调用时执行的commit并没有使用命名空间调用,所以势必又会去调用最外层的mutation?所以根源是解构出的commit有问题,我们看下vuex源码这块是怎么写的?function installModule (store, rootState, path, module, hot) {
var isRoot = !path.length;
var namespace = store._modules.getNamespace(path);

var local = module.context = makeLocalContext(store, namespace, path);
//创建上下文对象代替store实例

module.forEachAction(function (action, key) {
var type = action.root ? key : namespace + key;
var handler = action.handler || action;
registerAction(store, type, handler, local);
});

module.forEachChild(function (child, key) {
installModule(store, rootState, path.concat(key), child, hot);
});
}复制代码registerActionfunction registerAction (store, type, handler, local) {
var entry = store._actions[type] || (store._actions[type] = []);
entry.push(function wrappedActionHandler (payload) {
//传入上下文对象
var res = handler.call(store, {
dispatch: local.dispatch,//只能调用当前模块的actions
commit: local.commit,//只能调用当前模块的mutations
getters: local.getters,//只包含当前模块的getters
state: local.state,//只包含当前的state
rootGetters: store.getters,//根上的getters,可用来调取别的模块中的getters
rootState: store.state//根上的state,可用来调取别的模块中的state
}, payload);
if (!isPromise(res)) {
res = Promise.resolve(res);
}
if (store._devtoolHook) {
return res.catch(function (err) {
store._devtoolHook.emit(‘vuex:error’, err);
throw err
})
} else {
return res
}
});
}复制代码创建上下文对象的方法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;
    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)
},

//调用a模块中的addAge,commit('addAge',payload)就相当于调用store.commit('a/addAge',payload)
commit: noNamespace ? store.commit : function (_type, _payload, _options) {
  //判断是否传入的对象
  //另外一种调用方式, 传入的是一个对象 commit({type:xx,payload:xx})
  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;
    if (process.env.NODE_ENV !== 'production' && !store._mutations[type]) {
      console.error(("[vuex] unknown local mutation type: " + (args.type) + ", global type: " + type));
      return
    }
  }
  //调用实例中的commit
  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
}复制代码unifyObjectStylefunction unifyObjectStyle (type, payload, options) {
//判断传入的第一个参数是否是对象,对参数重新做调整后返回
if (isObject(type) && type.type) {
options = payload
payload = type
type = type.type
}
assert(typeof type === ‘string’, Expects string as the type, but found ${typeof type}.)
return { type, payload, options }
}复制代码4.subscribe这个API可能很多人不常用,主要使用场景就是监听commit操作,比如我需要将哪些state值动态本地存储,即只要调用commit就执行本地存储将新的值更新。当然还有很多使用场景Store.prototype.commit = function commit (_type, _payload, _options) {

this._subscribers.forEach(function (sub) { return sub(mutation, thisKaTeX parse error: Expected 'EOF', got '}' at position 11: 1.state); }̲); ... };复制代码…store.subscribe(()=>{
console.log(‘subscribe’)
})复制代码每执行一次commit便会打印subscribe当然还有action的监听,可以自己研究下5.辅助函数方便调用的语法糖5.1 mapState原理其实很简单比如在a模块调用age,想办法将其指向this.KaTeX parse error: Expected '}', got 'EOF' at end of input: … return this.store.state.a.age
}
}
=>
computed:{
age(){
return this.$store.state.a.age
}
}复制代码于是我们调用this.age就可获得当前模块的state中的age值,好的我们看下源码var mapState = normalizeNamespace(function (namespace, states) {
var res = {};
//传入的states必须是数组或对象
if (process.env.NODE_ENV !== ‘production’ && !isValidMap(states)) {
console.error(’[vuex] mapState: mapper parameter must be either an Array or an Object’);
}
//遍历先规范化states,然后做遍历
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) {
    // 获取实例上_modulesNamespaceMap属性为namespace的module
    var module = getModuleByNamespace(this.$store, 'mapState', namespace);
    if (!module) {
      return
    }
    //module中的context是由makeLocalContext创建的上下文对象,只包含当前模块
    state = module.context.state;
    getters = module.context.getters;
  }
  //判断val是否是函数,这里涉及到mapState的不同用法,核心的语法糖
  return typeof val === 'function'
    ? val.call(this, state, getters)
    : state[val]
};
// mark vuex getter for devtools
res[key].vuex = true;

});
return res
});复制代码5.2 mapGetters原理和mapState几乎一模一样比如在a模块调用totalSalary,想办法将其指向this.KaTeX parse error: Expected '}', got '&' at position 159: …= 'production' &̲& !isValidMap(g…store, ‘mapGetters’, namespace)) {
return
}
if (process.env.NODE_ENV !== ‘production’ && !(val in this. s t o r e . g e t t e r s ) ) c o n s o l e . e r r o r ( ( " [ v u e x ] u n k n o w n g e t t e r : " + v a l ) ) ; r e t u r n r e t u r n t h i s . store.getters)) { console.error(("[vuex] unknown getter: " + val)); return } return this. store.getters[val] //这个在上面命名空间中的getters已经讲过了,核心语法糖
};
//调试用的
res[key].vuex = true;
});
return res
});复制代码5.3 mapMutations用法:…mapMutations(‘a’,[‘addAge’])
//…mapMutations(‘a’,{
// addAge:(commit,payload)=>{
// commit(‘addAge’,payload)
// }
//第二种调用方法
})复制代码var mapMutations = normalizeNamespace(function (namespace, mutations) {
var res = {};
if (process.env.NODE_ENV !== ‘production’ && !isValidMap(mutations)) {
console.error(’[vuex] mapMutations: mapper parameter must be either an Array or an Object’);
}
normalizeMap(mutations).forEach(function (ref) {
var key = ref.key;
var val = ref.val;

res[key] = function mappedMutation () {
  //拼接传来的参数
  var args = [], len = arguments.length;
  while ( len-- ) args[ len ] = arguments[ len ];

  // Get the commit method from store
  var commit = this.$store.commit;
  if (namespace) {
    var 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
});复制代码5.4 mapActions同mapMutations,只要把函数内部的commit换成dispatch就行了5.5 辅助函数中涉及的几个方法normalizeMap规范化传入的参数function normalizeMap (map) {
if (!isValidMap(map)) {
return []
} //不是数组或者对象直接返回空数组
return Array.isArray(map)
? map.map(function (key) { return ({ key: key, val: key }); })
: Object.keys(map).map(function (key) { return ({ key: key, val: map[key] }); })
//对传入的参数做处理,做统一输出
}复制代码normalizeNamespace标准化命名空间,是一个科里化函数function normalizeNamespace (fn) {
return function (namespace, map) {
// 规范传入的参数
// 我们一般这样调用 有模块:…mapState(‘a’,[‘age’])或者 没模块:…mapState([‘age’])
if (typeof namespace !== ‘string’) { //没传入命名空间的话,传入的第一个值赋值给map,命名空间为空
map = namespace;
namespace = ‘’;
} else if (namespace.charAt(namespace.length - 1) !== ‘/’) {
namespace += ‘/’;
//传入命名空间的话,看命名空间最后是否有/,没有添加/
}
return fn(namespace, map)
}
}复制代码对传入的命名空间添加/是因为每个声明命名空间的模块,内部的getters等的属性名都经过了处理,以命名空间+key的形式存储。最后最后附上一些心得,其实源码并不难读懂,核心代码和逻辑就那么点,我们完全可以实现一个极其简易版的vuex源码之所以看上去很繁杂是因为作者将很多方法抽离出来,方法单独命名,所以经常会发现函数里又套了n个函数,其实只是作者将重复的一些方法单独抽离出来公用而已多个API以及API的不同使用方法,比如上面提到的mapState这些辅助函数,可以有多种使用方式,还有比如commit你可以传{xx,payload}也可以传{type:xx,payload:xx},作者要做许多类似这样的处理,兼容多个用户的习惯庞大的错误处理,什么throw new Error这些东西其实也占了很大比重,好的源码设计错误处理是必不可少的,这是为了防止使用者做一些违背作者设计初衷的行为,如果错误提示设计的好是能起到锦上添花的作用的,一旦出了错误用户能一目了然哪出了bug。

发布了60 篇原创文章 · 获赞 10 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/a59612/article/details/104348413