自已建网站微信登录,珠海绿网科技有限公司,做网站开发的有哪些公司,成都网站关键词推广vuex源码分析 了解vuex 什么是vuex vuex是一个为vue进行统一状态管理的状态管理器#xff0c;主要分为state, getters, mutations, actions几个部分#xff0c;vue组件基于state进行渲染#xff0c;当state发生变化时触发组件的重新渲染#xff0c;并利用了vue的响应式原理…vuex源码分析 了解vuex 什么是vuex vuex是一个为vue进行统一状态管理的状态管理器主要分为state, getters, mutations, actions几个部分vue组件基于state进行渲染当state发生变化时触发组件的重新渲染并利用了vue的响应式原理衍生出getters,getters以state作为基础进行不同形式的数据的构造当state发生改变时响应式的进行改变。state的改变只能够由commit进行触发每次的改变都会被devtools记录。异步的操作通过actions触发比如后台api请求发送等等异步操作完成时获取值并触发mutations事件进而实现stat重新求值触发视图重新渲染。 为什么需要vuex 解决组件间的通信和传统事件模式过长的调用链难以调试的问题在vue的使用中我们利用vue提供的事件模式实现父子间的通信或者利用eventBus的方式进行多组件之间的通行但是随着项目变得庞大调用链有时会变的很长会无法定位到事件的发起者并且基于事件模式的调试是会让开发者头疼不已下一个接手项目的人很难知道一个事件的触发会带来哪些影响vuex将状态层和视图层进行抽离所有的状态得到统一的管理所有的组件共享一个state有了vuex我们的关注从事件转移到了数据我们可以只关心哪些组件引用了状态中的某个值devtools实时反应state的当前状态让调试变得简单。另外组件间的通信从订阅同一个事件转移到了共享同一个数据变得更加简易。 解决父子组件间数据传递问题在vue的开发中我们会通过props或者inject去实现父子组件的数据传递但是当组件层级过深时props的传递会带来增加冗余代码的问题中间一些不需特定数据的组件为了进行数据传递会注入不必要的数据而inject的数据传递本来就是有缺陷的当代码充斥着各种provided和inject时杂乱的根本不知道组件inject的数据是在哪里provide进来的。vuex将一些公用数据抽离并统一管理后直接让这种复杂的数据传递变得毫不费力。 一 install 为了实现通过Vue.use()方法引入vuex需要为vuex定义一个install方法。vuex中的intall方法主要作用是将store实例注入到每一个vue组件中具体实现方式如下
export function install (_Vue) {// 避免重复安装if (Vue amp;amp; Vue _Vue) {// 开发环境报错console.warn(duplicate install);}Vue _Vue;// 开始注册全局mixinapplyMixin(Vue);
} 以上代码中通过定义一个全局变量Vue保存当前的引入的Vue来避免重复安装然后通过apllyMixin实现将store注入到各个实例中去
export default function (Vue) {// 获取vue版本const version Number(Vue.version.split(.)[0]);// 根据版本选择注册方式if (version gt; 2) {// 版本大于2在mixin中执行初始化函数Vue.mixin({ beforeCreate: vuexInit });} else {// 低版本将初始化方法放在options.init中执行const _init Vue.prototype._init;Vue.prototype._init function (options {}) {options.init options.init? [vuexInit].concat(options.init): vuexInit;_init();};}// 初始化函数:将store作为属性注入到所有组件中function vuexInit () {// 根组件if (this.$options amp;amp; this.$options.store) {this.$store typeof this.$options.store function? this.$options.store(): this.$options.store;} else if (this.$options.parent amp;amp; this.$options.parent.$store) { // 非根组件this.$store this.$options.parent.$store;}}
} 首先看这段代码核心逻辑实现的关键函数vuexInit该函数首先判断this.$options选项该选项在根实例实例化时传入new Vue(options:Object)
new Vue({store
}) 中是否包含store属性如果有则将实例的this.$store属性指向this.$options.store,如果没有则指向this.$parent即父实例中的$store。此时我们在install执行后通过在实例化根组件时把store传入options就能将所有子组件的$store属性都指向这个store了。此外需要注意的时applyMixin执行时首先会判断当前Vue的版本号版本2以上通过mixin混入的方式在所有组件实例化的时候执行vueInit而版本2以下则通过options.init中插入执行的方式注入。以下时安装函数的几点总结 避免重复安装判断版本不同版本用不同方式注入初始方法2之前通过options.init注入2之后通过mixin注入将store注入到所有vue的实例属性$store中二、如何实现一个简单的commit commit实际上就是一个比较简单的发布-订阅模式的实现不过这个过程中会涉及module的实现state与getters之间响应式的实现方式并为之后介绍actions可以做一些铺垫 使用 首先回顾下commit的使用
// 实例化store
const store new Vuex.Store({state: { count: 1 },mutations: {add (state, number) {state.count number;}}
}); 实例化store时参数中的mutation就是事件队列中的事件每个事件传入两个参数分别时state和payload,每个事件实现的都是根据payload改变state的值
lt;templategt;lt;divgt;count:{{state.count}}lt;button clickaddgt;addlt;/buttongt;lt;/divgt;
lt;/templategt;lt;scriptgt;export default {name: app,created () {console.log(this);},computed: {state () {return this.$store.state;}},methods: {add () {this.$store.commit(add, 2);}}};
lt;/scriptgt;lt;style scopedgt;lt;/stylegt; 我们在组件中通过commit触发相应类型的mutation并传入一个payload此时state会实时发生变化 实现 首先来看为了实现commit我们在构造函数中需要做些什么
export class Store {constructor (options {}) {// 声明属性this._mutations Object.create(null);this._modules new ModuleCollection(options);// 声明发布函数const store this;const { commit } this;this.commit function (_type, _payload, _options) {commit.call(store, _type, _payload, _options);};const state this._modules.root.state;// 安装根模块this.installModule(this, state, [], this._modules.root);// 注册数据相应功能的实例this.resetStoreVm(this, state);} 首先是三个实例属性_mutations是发布订阅模式中的事件队列_modules属性用来封装传入的options:{state, getters, mutations, actions}为其提供一些基础的操作方法commit方法用来触发事件队列中相应的事件然后我们会在installModule中注册事件队列在resetStoreVm中实现一个响应式的state。 modules 在实例化store时我们会传入一个对象参数这里面包含state,mutations,actions,getters,modules等数据项我们需要对这些数据项进行封装并暴露一个这个些数据项的操作方法这就是Module类的作用另外在vuex中有模块的划分需要对这些modules进行管理由此衍生出了ModuleCollection类本节先专注于commit的实现对于模块划分会放在后面讨论对于直接传入的statemutationsactionsgetters在vuex中会先通过Module类进行包装然后注册在ModuleCollection的root属性中
export default class Module {constructor (rawModule, runtime) {const rawState rawModule.state;this.runtime runtime;// 1.todo:runtime的作用是啥this._rawModule rawModule;this.state typeof rawState function ? rawState() : rawState;}// 遍历mumation,执行函数forEachMutation (fn) {if (this._rawModule.mutations) {forEachValue(this._rawModule.mutations, fn);}}
}
export function forEachValue (obj, fn) {Object.keys(obj).forEach((key) gt; fn(obj[key], key));
} 构造函数中传入的参数rawModule就是{statemutationsactionsgetters}对象,在Module类中定义两个属性_rawModule用于存放传入的rawModule,forEachMutation实现mutations的遍历执行将mutation对象的value,key传入fn并执行接下去将这个module挂在modulecollection的root属性上
export default class ModuleCollection {constructor (rawRootModule) {// 注册根module入参path,module,runtimethis.register([], rawRootModule, false);}// 1.todo runtime的作用register (path, rawRootModule, runtime) {const module new Module(rawRootModule, runtime);this.root module;}
} 经过这样一系列的封装this._modules属性就是下面这样的数据结构 state 由于mutations中保存的所有事件都是为了按一定规则改变state所以我们要先介绍下store是如何进行state的管理的尤其是如何通过state的改变响应式的改变getters中的值在构造函数中提到过一个方法resetStoreVm在这个函数中会实现state和getters的响应式关系 resetStoreVm (store, state) {const oldVm store._vm;// 注册store._vm new Vue({data: {$$state: state}});// 注销旧实例if (oldVm) {Vue.nextTick(() gt; {oldVm.destroy();});}} 这个函数传入两个参数分别为实例本身和state首先注册一个vue实例保存在store实例属性_vm上其中data数据项中定义了$$state属性指向state后面会介绍将getters分解并放在computed数据项中这样很好的利用Vue原有的数据响应系统实现响应式的state并且赋新值之后会把老的实例注销。对于state的包装实际还差一步我们平常访问state的时候是直接通过store.state访问的如果不做处理现在我们只能通过store._vm.data.$$state来访问实际vuex通过class的getset属性实现state的访问和更新的
export class Store {get state () {return this._vm._data.$$state;}set state (v) {if (process.env.NODE_ENV ! production) {console.error(user store.replaceState());}}
} 值得注意的是我们不能直接对state进行赋值而要通过store.replaceState赋值否则将会报错 事件注册 接下去终于要步入commit原理的核心了发布-订阅模式包含两个步骤事件订阅和事件发布首先来谈谈vuex是如何实现订阅过程的
export class Store {constructor (options {}) {// 声明属性this._mutations Object.create(null);// 为什么不直接赋值nullthis._modules new ModuleCollection(options);const state this._modules.root.state;// 安装根模块this.installModule(this, state, [], this._modules.root);}installModule (store, state, path, module) {// 注册mutation事件队列const local this.makeLocalContext(store, path);module.forEachMutation((mutation, key) gt; {this.registerMutation(store, key, mutation, local);});}// 注册mutationregisterMutation (store, type, handler, local) {const entry this._mutations[type] || (this._mutations[type] []);entry.push(function WrappedMutationHandler (payload) {handler.call(store, local.state, payload);});}
} 我们只截取相关的部分代码其中两个关键的方法installModule和registerMutation我们在此处会省略一些关于模块封装的部分此处的local可以简单的理解为一个{stategetters}对象事件注册的大致过程就是遍历mutation并将mutation进行包装后push进指定类型的事件队列首先通过Moulde类的实例方法forEachMutation对mutation进行遍历并执行registerMutation进行事件的注册在registerMutation中生成一个this._mutations指定类型的事件队列注册事件后的this._mutations的数据结构如下 事件发布 根据事件注册后this._mutations的结构我们可以很轻松的实现事件发布找到指定类型的事件队列遍历这个队列传入参数并执行。
// 触发对应type的mutationcommit (_type, _payload, _options) {// 获取参数const {type,payload} unifyObjectStyle(_type, _payload, _options);const entry this._mutations[type];// 遍历触发事件队列entry.forEach(function commitIterator (handler) {handler(payload);});} 但是需要注意的是首先需要对参数进行下处理就是unifyObjectStyle干的事情
// 入参规则type可以是带type属性的对象也可以是字符串
function unifyObjectStyle (type, payload, options) {if (isObject(type)) {payload type;options payload;type type.type;}return { type, payload, options };
} 其实实现了type可以为字符串也可以为对象当为对象是内部使用的type就是type.type而第二个参数就变成了type第三个参数变成了payload。到此关于commit的原理已经介绍完毕所有的代码见分支 https://github.com/miracle931... 三、action和dispatch原理 用法 定义一个action
add ({ commit }, number) {return new Promise((resolve, reject) gt; {setTimeout(() gt; {const pow 2;commit(add, Math.pow(number, pow));resolve(number);}, 1000);});} 触发action this.$store.dispatch(add, 4).then((data) gt; {console.log(data);}); 为什么需要action 有时我们需要触发一个异步执行的事件比如接口请求等但是如果依赖mutatoin这种同步执行的事件队列我们无法获取执行的最终状态。此时我们需要找到一种解决方案实现以下两个目标 一个异步执行的队列捕获异步执行的最终状态通过这两个目标我们可以大致推算该如何实现了只要保证定义的所有事件都返回一个promise再将这些promise放在一个队列中通过promise.all去执行返会一个最终状态的promise这样既能保证事件之间的执行顺序也能捕获最终的执行状态。 action和dispatch的实现 注册 首先我们定义一个实例属性_actions,用于存放事件队列
constructor (options {}) {// ...this._actions Object.create(null);// ...} 接着在module类中定义一个实例方法forEachActions,用于遍历执行actions
export default class Module {// ...forEachAction (fn) {if (this._rawModule.actions) {forEachValue(this._rawModule.actions, fn);}}// ...
} 然后在installModule时期去遍历actions,注册事件队列
installModule (store, state, path, module) {// ...module.forEachAction((action, key) gt; {this.registerAction(store, key, action, local);});// ...} 注册
registerAction (store, type, handler, local) {const entry this._actions[type] || (this._actions[type] []);entry.push(function WrappedActionHandler (payload, cb) {let res handler.call(store, {dispatch: local.dispatch,commit: local.commit,state: local.state,rootState: store.state}, payload, cb);// 默认action中返回promise如果不是则将返回值包装在promise中if (!isPromise(res)) {res Promise.resolve(res);}return res;});} 注册方法中包含四个参数store代表store实例type代表action类型handler是action函数。首先判断是否已存在该类型acion的事件队列如果不存在则需要初始化为数组。然后将该事件推入指定类型的事件队列。需要注意的两点第一action函数访问到的第一个参数为一个context对象第二事件返回的值始终是一个promise。 发布
dispatch (_type, _payload) {const {type,payload} unifyObjectStyle(_type, _payload);// ??todo 为什么是一个事件队列何时会出现一个key对应多个actionconst entry this._actions[type];// 返回promise,dispatch().then()接收的值为数组或者某个值return entry.length gt; 1? Promise.all(entry.map((handler) gt; handler(payload))): entry[0](payload);} 首先获取相应类型的事件队列然后传入参数执行返回一个promise当事件队列中包含的事件个数大于1时将返回的promise保存在一个数组中然后通过Pomise.all触发当事件队列中的事件只有一个时直接返回promise这样我们就可以通过dispatch(type, payload).then(data{})得到异步执行的结果此外事件队列中的事件触发通过promise.all实现两个目标都已经达成。 getters原理 getters的用法 在store实例化时我们定义如下几个选项
const store new Vuex.Store({state: { count: 1 },getters: {square (state, getters) {return Math.pow(state.count, 2);}},mutations: {add (state, number) {state.count number;}}
});首先我们在store中定义一个stategetters和mutations其中state中包含一个count初始值为1getters中定义一个square该值返回为count的平方在mutations中定义一个add事件当触发add时count会增加number。接着我们在页面中使用这个store
lt;templategt;lt;divgt;lt;divgt;count:{{state.count}}lt;/divgt;lt;divgt;getterCount:{{getters.square}}lt;/divgt;lt;button clickaddgt;addlt;/buttongt;lt;/divgt;
lt;/templategt;lt;scriptgt;export default {name: app,created () {console.log(this);},computed: {state () {return this.$store.state;},getters () {return this.$store.getters;}},methods: {add () {this.$store.commit(add, 2);}}};
lt;/scriptgt;lt;style scopedgt;lt;/stylegt; 执行的结果是我们每次触发add事件时state.count会相应增2,而getter始终时state.count的平方。这不由得让我们想起了vue中的响应式系统data和computed之间的关系貌似如出一辙实际上vuex就是利用vue中的响应式系统实现的。 getters的实现 首先定义一个实例属性_wappedGetters用来存放getters
export class Store {constructor (options {}) {// ...this._wrappedGetters Object.create(null);// ...}
} 在modules中定义一个遍历执行getters的实例方法并在installModule方法中注册getters并将getters存放至_wrappedGetters属性中
installModule (store, state, path, module) {// ...module.forEachGetters((getter, key) gt; {this.registerGetter(store, key, getter, local);});// ...}
registerGetter (store, type, rawGetters, local) {// 处理getter重名if (this._wrappedGetters[type]) {console.error(duplicate getter);}// 设置_wrappedGetters用于this._wrappedGetters[type] function wrappedGetterHandlers (store) {return rawGetters(local.state,local.getters,store.state,store.getters);};} 需要注意的是vuex中不能定义两个相同类型的getter在注册时我们将一个返回选项getters执行结果的函数传入的参数为store实例选项中的getters接受四个参数分别为作用域下和store实例中的state和getters关于local的问题在之后module原理的时候再做介绍在此次的实现中local和store中的参数都是一致的。之后我们需要将所有的getters在resetStoreVm时期注入computed并且在访问getters中的某个属性时将其代理到store.vm中的相应属性
// 注册响应式实例resetStoreVm (store, state) {// 将store.getters[key]指向store._vm[key],computed赋值forEachValue(wrappedGetters, function (fn, key) {computed[key] () gt; fn(store);});// 注册store._vm new Vue({data: {$$state: state},computed});// 注销旧实例if (oldVm) {Vue.nextTick(() gt; {oldVm.destroy();});}} 在resetStroreVm时期遍历wrappedGetters并将getters包装在一个具有相同key的computed中再将这个computed注入到store._vm实例中。
resetStoreVm (store, state) {store.getters {};forEachValue(wrappedGetters, function (fn, key) {// ...Object.defineProperty(store.getters, key, {get: () gt; store._vm[key],enumerable: true});});// ...} 然后将store.getters中的属性指向store._vm中对应的属性也就是store.computed中对应的属性这样当store._vm中data.$$state(store.state)发生变化时引用state的getter也会实时计算以上就是getters能够响应式变化的原理具体代码见 https://github.com/miracle931... helpers原理 helpers.js中向外暴露了四个方法分别为mapState,mapGetters,mapMutations和mapAction。这四个辅助方法帮助开发者在组件中快速的引用自己定义的state,getters,mutations和actions。首先了解其用法再深入其原理
const store new Vuex.Store({state: { count: 1 },getters: {square (state, getters) {return Math.pow(state.count, 2);}},mutations: {add (state, number) {state.count number;}},actions: {add ({ commit }, number) {return new Promise((resolve, reject) gt; {setTimeout(() gt; {const pow 2;commit(add, Math.pow(number, pow));resolve(number);}, 1000);});}}
}); 以上是我们定义的store
lt;templategt;lt;divgt;lt;divgt;count:{{count}}lt;/divgt;lt;divgt;getterCount:{{square}}lt;/divgt;lt;button clickmutAdd(1)gt;mutAddlt;/buttongt;lt;button clickactAdd(1)gt;actAddlt;/buttongt;lt;/divgt;
lt;/templategt;lt;scriptgt;import vuex from ./vuex/src;export default {name: app,computed: {...vuex.mapState([count]),...vuex.mapGetters([square])},methods: {...vuex.mapMutations({ mutAdd: add }),...vuex.mapActions({ actAdd: add })}};
lt;/scriptgt;lt;style scopedgt;lt;/stylegt; 然后通过mapXXX的方式将store引入组件并使用。观察这几个方法的引用方式可以知道这几个方法最终都会返回一个对象对象中所有的值都是一个函数再通过展开运算符把这些方法分别注入到computed和methods属性中。对于mapState和mapGetters而言返回对象中的函数执行后会返回传入参数对应的值return store.state[key];或者return store.getters[key]而对于mapMutations和mapActions而言返回对象中的函数将执行commit[key],payload或者dispatch[key],payload这就是这几个方法的简单原理接下去将一个个分析vuex中的实现 mapState和mapGetters
export const mapState function (states) {// 定义一个返回结果mapconst res {};// 规范化statenormalizeMap(states).forEach(({ key, val }) gt; {// 赋值res[key] function mappedState () {const state this.$store.state;const getters this.$store.getters;return typeof val function? val.call(this, state, getters): state[val];};});// 返回结果return res;
}; 首先看mapsState最终的返回值res是一个对象传入的参数是我们想要map出来的几个属性mapState可以传入一个字符串数组或者是对象数组字符串数组中包含的是引用的属性对象数组包含的是使用值与引用的映射这两种形式的传参我们需要通过normalizeMap进行规范化统一返回一个对象数组
function normalizeMap (map) {return Array.isArray(map)? map.map(key gt; ({ key, val: key })): Object.keys(map).map(key gt; ({ key, val: map[key] }))
} normalizeMap函数首先判断传入的值是否为数组若是则返回一个key和val都为数组元素的对象数组如果不是数组则判断传入值为一个对象接着遍历该对象返回一个以对象键值为key和val值的对象数组。此时通过normalizeMap之后的map都将是一个对象数组。接着遍历规范化之后的数组对返回值对象进行赋值赋值函数执行后返回state对应key的值如果传入值为一个函数则将getters和state作为参数传入并执行最终返回该对象这样在computed属性中展开后就能直接通过key来引用对应state的值了。mapGetters与mapState的实现原理基本一致
export const mapGetters function (getters) {const res {};normalizeMap(getters).forEach(({ key, val }) gt; {res[key] function mappedGetter () {return this.$store.getters[val];};});return res;
}; mapActions和mapMutations
export const mapActions function (actions) {const res {};normalizeMap(actions).forEach(({ key, val }) gt; {res[key] function (...args) {const dispatch this.$store.dispatch;return typeof val function? val.apply(this, [dispatch].concat(args)): dispatch.apply(this, [val].concat(args));};});return res;
}; mapActions执行后也将返回一个对象对象的key用于组件中引用对象中value为一个函数该函数传参是dispatch执行时的payload其中val如果不是一个函数则判断其为actionType通过dispath(actionType,payload)来触发对应的action如果传入的参数为一个函数则将dispatch和payload作为参数传入并执行这样可以实现在mapAction时组合调用多个action或者自定义一些其他行为。最终返回该对象在组件的methods属性中展开后可以通过调用key对应的函数来触发action。mapMutation的实现原理与mapActions大同小异
export const mapMutations function (mutations) {const res {};normalizeMap(mutations).forEach(({ key, val }) gt; {res[key] function mappedMutation (...args) {const commit this.$store.commit;return typeof val function? val.apply(this, [commit].concat(args)): commit.apply(this, [val].concat(args));};});return res;
}; module 为了方便进行store中不同功能的切分在vuex中可以将不同功能组装成一个单独的模块模块内部可以单独管理state也可以访问到全局状态。 用法
// main.js
const store new Vuex.Store({state: {},getters: {},mutations: {},actions: {},modules: {a: {namespaced: true,state: { countA: 9 },getters: {sqrt (state) {return Math.sqrt(state.countA);}},mutations: {miner (state, payload) {state.countA - payload;}},actions: {miner (context) {console.log(context);}}}}
});
//app.vue
lt;templategt;lt;divgt;lt;divgt;moduleSqrt:{{sqrt}}lt;/divgt;lt;divgt;moduleCount:{{countA}}lt;/divgt;lt;button clickminer(1)gt;modMutAddlt;/buttongt;lt;/divgt;
lt;/templategt;lt;scriptgt;import vuex from ./vuex/src;export default {name: app,created () {console.log(this.$store);},computed: {...vuex.mapGetters(a, [sqrt]),...vuex.mapState(a, [countA])},methods: {...vuex.mapMutations(a, [miner])}};
lt;/scriptgt;lt;style scopedgt;lt;/stylegt; 上述代码中我们定义了一个key为a的module将其namespaced设置成了true对于namespacefalse的模块它将自动继承父模块的命名空间。对于模块a他有以下几点特性 拥有自己独立的stategetters和actions中能够访问到state,gettersrootState, rootGettersmutations中只能改变模块中的state根据以上特性可以将之后的module的实现分为几个部分 用什么样的数据格式存放module如何创建一个模块的context实现statecommit, dispatch getters的封装并且让commit只改变内部的state另外让模块中的gettersdispatch保持对根模块的可访问性 如何进行模块中getters, mutations, actions的注册让其与namespace进行绑定辅助方法该如何去找到namespace下gettersmutations和actions,并将其注入组件中构造嵌套的module结构 vuex最后构造出的module是这样的一种嵌套的结构第一级是一个root之后的的每一级都有一个_rawModule和_children属性分别存放自身的gettersmutations和actions和子级。实现这样的数据结构用一个简单的递归便可以完成首先是我们的入参大概是如下的结构
{state: {},getters: {},mutations: {},actions: {},modules: {a: {namespaced: true,state: {},getters: {},mutations: {},actions: {}},b: {namespaced: true,state: {},getters: {},mutations: {},actions: {}}}
} 我们会在store的构造函数中将这个对象作为ModuleCollection实例化的参数
export class Store {constructor (options {}) {this._modules new ModuleCollection(options);}
} 所有的嵌套结构的构造都在ModuleCollection实例化的过程中进行
// module-collection.js
export default class ModuleCollection {constructor (rawRootModule) {// 注册根module入参path,module,runtimethis.register([], rawRootModule, false);}// 根据路径获取模块从root开始搜索get (path) {return path.reduce((module, key) gt; module.getChild(key), this.root);}// 1.todo runtime的作用register (path, rawModule, runtime true) {// 生成moduleconst newModule new Module(rawModule, runtime);if (path.length 0) { // 根模块注册在root上this.root newModule;} else { // 非根模块获取父模块挂载const parent this.get(path.slice(0, -1));parent.addChild(path[path.length - 1], newModule);}// 模块上是否含有子模块有则注册子模块if (rawModule.modules) {forEachValue(rawModule.modules, (newRawModule, key) gt; {this.register(path.concat(key), newRawModule, runtime);});}}
}
// module.js
export default class Module {addChild (key, module) {this._children[key] module;}
} 实例化时首先会执行register函数在register函数中根据传入的rawModule创建一个Module的实例然后根据注册的路径判断是否为根模块如果是则将该module实例挂载在root属性上如果不是则通过get方法找到该模块的父模块将其通过模块的addChild方法挂载在父模块的_children属性上最后判断该模块是否含有嵌套模块如果有则遍历嵌套模块递归执行register方法这样就能构造如上图所示的嵌套模块结构了。有了以上这样的结构我们可以用reduce方法通过path来获取指定路径下的模块也可以用递归的方式对所有的模块进行统一的操作大大方便了模块的管理。 构造localContext 有了基本的模块结构后下面的问题就是如何进行模块作用域的封装了让每个模块有自己的state并且对于这个state有自己管理这个state的方法并且我们希望这些方法也能够访问到全局的一些属性。总结一下现在我们要做的事情
// module
{state: {},getters: {}...modules:{n1:{namespaced: true,getters: {g(state, rootState) {state.s // gt; state.n1.srootState.s // gt; state.s}},mutations: {m(state) {state.s // gt; state.n1.s}},actions: {a({state, getters, commit, dispatch}) {commit(m); // gt; mutations[n1/m]dispatch(a1); // gt; actions[n1/a1]getters.g // gt; getters[n1/g]},a1(){}}}}
} 在namespacedtrue的模块中访问到的state,getters都是自模块内部的state和getters,只有rootState,以及rootGetters指向根模块的state和getters另外在模块中commit触发的都是子模块内部的mutationsdispatch触发的都是子模块内部的actions。在vuex中通过路径匹配去实现这种封装。
//state
{s: anyn1: {s: any,n2: {s: any}}
}
// getters
{g: function () {},n1/g: function () {},n1/n2/g: function () {}
}
// mutations
{m: function () {},n1/m: function () {},n1/n2/m: function () {}
}
// actions
{a: function () {},n1/a: function () {},n1/n2/a: function () {}
} vuex中要构造这样一种数据结构去存储各个数据项然后将context中的commit方法重写将commit(type)代理至namespaceType以实现commit方法的封装类似的dispatch也是通过这种方式进行封装而getters则是实现了一个getterProxy,将key代理至store.getters[namespacekey]上然后在context中的getters替换成该getterProxy而state则是利用了以上这种数据结构直接找到对应path的state赋给context.state这样通过context访问到的都是模块内部的数据了。接着来看看代码实现
installModule (store, state, path, module, hot) {const isRoot !path.length;// 获取namespaceconst namespace store._modules.getNamespace(path);} 所有数据项的构造以及context的构造都在store.js的installModule方法中首先通过传入的path获取namespace // 根据路径返回namespacegetNamespace (path) {let module this.root;return path.reduce((namespace, key) gt; {module module.getChild(key);return namespace (module.namespaced ? ${key}/ : );}, );} 获取namespace的方法是ModuleCollections的一个实例方法它会逐层访问modules,判断namespaced属性若为true则将path[index]拼在namespace上这样就获得了完整的namespace之后是嵌套结构state的实现
installModule (store, state, path, module, hot) {// 构造嵌套stateif (!isRoot amp;amp; !hot) {const moduleName path[path.length - 1];const parentState getNestedState(state, path.slice(0, -1));Vue.set(parentState, moduleName, module.state);}} 首先根据出path获取state上对应的parentState,此处入参state就是store.state
function getNestedState (state, path) {return path.length? path.reduce((state, key) gt; state[key], state): state
} 其中的getNestState用于根据路径获取相应的state,在获取parentState之后将module.state挂载在parentState[moduleName]上。这样就构造了一个如上说所述的嵌套state结构。在得到namespace之后我们需要将传入的gettersmutationsactions根据namespace去构造了
installModule (store, state, path, module, hot) {module.forEachMutation((mutation, key) gt; {const namespacdType namespace key;this.registerMutation(store, namespacdType, mutation, local);});module.forEachAction((action, key) gt; {const type action.root ? type : namespace key;const handler action.handler || action;this.registerAction(store, type, handler, local);});module.forEachGetters((getter, key) gt; {const namespacedType namespace keythis.registerGetter(store, namespacedType, getter, local);});} gettersmutationsactions的构造有着几乎一样的方式只不过分别挂载在store._getters,store._mutations,stors._actions上而已因此我们值分析mutations的构造过程。首先是forEachMutation遍历module中的mutations对象然后通过ergisterMustions注册到以namespacekey的key上
function registerMutation (store, type, handler, local) {const entry store._mutations[type] || (store._mutations[type] [])entry.push(function wrappedMutationHandler (payload) {handler.call(store, local.state, payload)// mutation中第一个参数是state第二个参数是payload})
} 实际上会存放在store._mutations[namespacekey]上。通过上述操作我们已经完成了封装的一半接下来我们还要为每个module实现一个context在这个context里面有stategetters,commit和actions但是这里的state,getters只能访问module里面的state和getters而commit和actions也只能触达到module内部的state和getters
installModule (store, state, path, module, hot) {// 注册mutation事件队列const local module.context makeLocalContext(store, namespace, path);} 我们会在installModule里面去实现这个context,然后将组装完的context分别赋给local和module.context而这个local在会在register的时候传递给getters,mutations, actions作为参数
function makeLocalContext (store, namespace, path) {const noNamespace namespace ;const local {dispatch: noNamespace? store.dispatch: (_type, _payload, _options) gt; {const args unifyObjectStyle(_type, _payload, _options);let { type } args;const { payload, options } args;if (!options || !options.root) {type namespace type;}store.dispatch(type, payload, options);},commit: noNamespace? store.commit: (_type, _payload, _options) gt; {const args unifyObjectStyle(_type, _payload, _options);let { type } args;const { payload, options } args;if (!options || !options.root) {type namespace type;}store.commit(type, payload, options);}};return local;
} 首先看context中的commit和dispatch方法的实现两者实现方式大同小异我们只分析commit首先通过namespace判断是否为封装模块如果是则返回一个匿名函数该匿名函数首先进行参数的规范化之后会调用store.dispatch而此时的调用会将传入的type进行偷换换成namespacetype所以我们在封装的module中执行的commit[type]实际上都是调用store._mutations[namespacetype]的事件队列
function makeLocalContext (store, namespace, path) {const noNamespace namespace ;Object.defineProperties(local, {state: {get: () gt; getNestedState(store.state, path)},getters: {get: noNamespace? () gt; store.getters: () gt; makeLocalGetters(store, namespace)}});return local;
} 然后是state通过local.state访问到的都是将path传入getNestedState获取到的state,实际上就是module内的state,而getters则是通过代理的方式实现访问内部getters的
function makeLocalGetters (store, namespace) {const gettersProxy {}const splitPos namespace.lengthObject.keys(store.getters).forEach(type gt; {// skip if the target getter is not match this namespaceif (type.slice(0, splitPos) ! namespace) return// extract local getter typeconst localType type.slice(splitPos)// Add a port to the getters proxy.// Define as getter property because// we do not want to evaluate the getters in this time.Object.defineProperty(gettersProxy, localType, {get: () gt; store.getters[type],enumerable: true})})return gettersProxy
} 首先声明一个代理对象gettersProxy之后遍历store.getters判断是否为namespace的路径全匹配如果是则将gettersProxy的localType属性代理至store.getters[type]然后将gettersProxy返回这样通过local.getters访问的localType实际上就是stores.getters[namespacetype]了。以下是一个小小的总结获取路径对应的命名空间namespacedtrue时拼上-state拼接到store.state上使其成为一个基于path的嵌套结构-注册localContext注册localContext Dispatch:namespace-扁平化参数-无root条件直接触发namespacetype-有root或hot条件触发typecommit-扁平化参数-无root条件直接触发namespacetype-有root或hot条件触发typeState:根据path查找stateGetters:声明代理对象遍历store.getters对象匹配key和namespace,命中后将其localType指向全路径原文地址https://segmentfault.com/a/1190000017219468 转载于:https://www.cnblogs.com/lovellll/p/10121808.html