vuex是专门为vuejs应用设计的一套状态管理模式,提供了一套集中管理数据的概念和用法,用来解决中大型项目中组件间大量数据共享的问题。它的核心概念包括state、mutations、actions,基于这些它的工做流程是: vue
vuex最终export了一个对象这个对象包括了一个install方法和一个类Store, 注意对应咱们的使用方法vuex
let store = new Vuex.Store({})
复制代码
install方法供Vue.use()使用,内部使用Vue.mixin在每个组件的beforeCreate生命周期中给组件混入了一个$store属性,这个属性就是那个惟一的store。编程
从new Vuex.Store(options)开始,有如下主要流程:api
除了以上内容以外vuex还导出了一些辅助函数好比mapState、mapMutations、mapActions。数组
严格模式下,mutation只能是同步。非严格模式下,若是你硬要使用异步也能够,固然不建议这么作。浏览器
若是咱们使用异步的方法来更改状态,设想一下,咱们但愿使用log来记录状态更改来源,当两个组件同时修改同一个状态,对应有两个log, 那么这两个log到底分别对应的是哪一个组件呢?bash
由于是异步的因此这两个修改动做的前后顺序没法保证,那么咱们根本没法判断log和组件的对应关系,可能你觉得是这个组件的log,实际上它是另外一个组件的,这就让咱们没办法去精确的进行调试和跟踪状态更改信息。异步
很简单就是直接使用 new Vue() 构造了一个Vue实例:async
this.vm = new Vue({
data: {
state: options.state// 响应化处理
}
});
复制代码
这样state就是响应式的了,看到这其实你就应该明白为何Vuex只能用在Vue项目中了。函数
当咱们在访问state时还须要一层代理:
get state() {// 获取实例上的state属性就会执行此方法
return this.vm.state
}
复制代码
固然这还没结束,以上只是针对最外层的state,那么若是咱们写了modules,modules内部的模块的state是怎么处理的呢?
从这开始咱们就接触到了Vuex最核心的东西了,上面咱们说了new一个Store实例时会先将用户传入的数据进行格式化,这就是register方法主要作的事,下面咱们就看看他究竟是怎么格式化?格式化最终的结果是什么?
咱们举一个例子:
let store = new Vuex.Store({
state: {// 单一数据源
age: 10
},
getters: {
myAge(state) {
return state.age + 20;
}
},
strict: true,
// 严格模式下只能使用同步
mutations: {
syncChange(state, payload) {
state.age += payload;
}
},
actions: {
asyncChange({commit}, payload) {
setTimeout(() => {
commit('syncChange', payload);
}, 1000);
}
},
modules: {
a: {
namespaced: true,
state: {
age: 'a100'
},
mutations: {
syncChange() {
console.log('a-syncChange');
}
}
},
b: {
namespaced: true,
state: {
age: 'b100'
},
mutations: {
syncChange() {
console.log('b-syncChange');
}
},
modules: {
c: {
namespaced: true,
state: {
age: 'c100'
},
mutations: {
syncChange() {
console.log('c-syncChange');
}
},
}
}
}
}
});
复制代码
先看看最终的结果
{
_raw: rootModule,
state: rootModule.state,
_children: {
a: {
_raw: aModule,
state: aModule.state,
_children: {}
},
b: {
_raw: aModule,
state: aModule.state,
_children: {
c: {
_raw: cModule,
state: cModule.state,
_children: {}
}
}
}
}
}
复制代码
能够看到,这仍是一个树形结构, _raw就是用户本身写的格式化以前的模块,state单独拿了出来是由于咱们在安装模块时会用到,_children放的就是modules下面的内容, 固然子模块下还有可能有孙子模块...
划重点,register
register(path, rootModule) {
let rawModule = {
_raw: rootModule,// 用户传入的模块定义
_children: {},
state: rootModule.state
}
rootModule.rawModule = rawModule;// 双向记录
if (!this.root) {
this.root = rawModule;
} else {
// 经典方法 找到当前模块的父亲
let parentModule = path.slice(0, -1).reduce((root, current) => { // 注册c的时候 [b, c].slice(0, -1) 至关于 [b, c].slice(0, 1) 的结果就是[b]
return root._children[current]
}, this.root);
parentModule._children[path[path.length-1]] = rawModule;
}
if (rootModule.modules) {
forEach(rootModule.modules, (moduleName, module) => {
// 注册a, [a] a模块的定义
// 注册b, [b] b模块的定义
// 注册c, [b, c] c模块的定义
this.register(path.concat(moduleName), module);
});
}
}
复制代码
register有两个参数,第一个是路径,数组类型,这里提一下,Vuex中判断模块层级关系就是使用数组,这也是一个经典通用的作法,下面会细说。第二个是根模块也就是进行格式化的起点,这里起点就是用户传入的数据。
往下咱们看到有这一句
rootModule.rawModule = rawModule;// 双向记录
复制代码
这个实际上是为了动态注册时用的,以后会说。
再往下就是经典的找爸爸了:
对于根模块,先定义一个根root,做为起点,接下来的子模块会先走forEach,使用path.concat(moduleName)
肯定模块层级关系,而后进行递归注册,这里的forEach方法是咱们本身封装的:
let forEach = (obj, callback) => {
Object.keys(obj).forEach(key => {
callback(key, obj[key]);
})
}
复制代码
注意slice的用法,slice(0, -1)就是取出除了当前模块以外的模块,不清楚slice用法的赶忙去补补吧~
找到当前模块的父模块以后就把当前模块放在父模块的_children中,这样一次父子模块的注册就算完成了。
好了,咱们回到正题,上面说了最外层的state已经实现了响应式,那么modules内部的state如何实现响应式处理?
这就又涉及到了Vuex的另外一个核心方法installModules方法了:
function installModule(store, rootState, path, rawModule) {// 安装时用的rawModule是格式化后的数据
// 安装子模块的状态
// 根据当前用户传入的配置 判断是否添加前缀
let root = store.modules.root // 获取到最终整个的格式化结果
let namespace = path.reduce((str, current) => {
root= root._children[current];// a
str = str + (root._raw.namespaced ? current + '/' : '');
return str;
}, '');
// console.log(path, namespace);
if(path.length > 0) {// 代表是子模块
// 若是是c,就先找到b
// [b,c,e] => [b, c] => c
let parentState = path.slice(0, -1).reduce((rootState, current) => {
return rootState[current];
}, rootState);
// vue的响应式不能对不存在的属性进行响应化
Vue.set(parentState, path[path.length-1], rawModule.state);
}
// 安装getters
let getters = rawModule._raw.getters;
if (getters) {
forEach(getters, (getterName, value) => {
Object.defineProperty(store.getters, namespace + getterName, {
get: () => {
// return value(rawModule.state);// rawModule就是当前的模块
return value(getState(store, path));
}
});
});
}
// 安装mutation
let mutations = rawModule._raw.mutations;
if (mutations) {
forEach(mutations, (mutationName, value) => {
let arr = store.mutations[namespace + mutationName] || (store. mutations[namespace + mutationName] = []);
arr.push((payload) => {
// value(rawModule.state, payload);// 真正执行mutation的地方
value(getState(store, path), payload);
store.subs.forEach(fn => fn({type: namespace + mutationName, payload: payload}, store.state)); // 这就用到了切片
});
});
}
// 安装action
let actions = rawModule._raw.actions;
if (actions) {
forEach(actions, (actionName, value) => {
let arr = store.actions[namespace + actionName] || (store.actions[namespace + actionName] = []);
arr.push((payload) => {
value(store, payload);
});
});
}
// 处理子模块
forEach(rawModule._children, (moduleName, rawModule) => {
installModule(store, rootState, path.concat(moduleName), rawModule)
});
}
复制代码
installModules方法有四个参数:
咱们如今只关注这一段代码:
if(path.length > 0) {// 代表是子模块
// 若是是c,就先找到b
// [b,c,e] => [b, c] => c
let parentState = path.slice(0, -1).reduce((rootState, current) => {
return rootState[current];
}, rootState);
// vue的响应式不能对不存在的属性进行响应化
Vue.set(parentState, path[path.length-1], rawModule.state);
}
复制代码
看到了吧,仍是找爸爸,以前是找父模块,此次是找父模块的状态。而后使用Vue.set()方法,对modules下的模块进行响应化处理。以后依旧是递归
// 处理子模块
forEach(rawModule._children, (moduleName, rawModule) => {
installModule(store, rootState, path.concat(moduleName), rawModule)
});
复制代码
基于以上, 咱们直接看installModules这一段代码:
// 安装getters
let getters = rawModule._raw.getters;
if (getters) {
forEach(getters, (getterName, value) => {
Object.defineProperty(store.getters, namespace + getterName, {
get: () => {
// return value(rawModule.state);// rawModule就是当前的模块
return value(getState(store, path));
}
});
});
}
复制代码
这里面涉及了两个新东西:
先说namespace,看下面这段代码
// 根据当前用户传入的配置 判断是否添加前缀
let root = store.modules.root // 获取到最终整个的格式化结果
let namespace = path.reduce((str, current) => {
root= root._children[current];// a
str = str + (root._raw.namespaced ? current + '/' : '');
return str;
}, '');
复制代码
仍是reduce,加上了命名空间以后原来的方法xxx就变成了a/b/xxx。
getState方法:
// 递归的获取每个模块的最新状态
function getState(store, path) {
let local = path.reduce((newState, current) => {
return newState[current];
}, store.state);
return local;
}
复制代码
仍是reduce,getState主要是结合Vuex实现数据持久化使用,下面咱们会介绍,这里先跳过。
ok, 大体了解了这两个东西以后,咱们再看getters, 能够看出最终全部的getter都使用Object.defineProperty定义在了store.getters这个对象中,注意这里至关于把原本的树形结构给铺平了
, 这也就是当咱们不适用namespace时,必定要保证不能重名的缘由。
// 安装mutation
let mutations = rawModule._raw.mutations;
if (mutations) {
forEach(mutations, (mutationName, value) => {
let arr = store.mutations[namespace + mutationName] || (store. mutations[namespace + mutationName] = []);
arr.push((payload) => {
value(getState(store, path), payload);
});
});
}
复制代码
知道了getters的原理mutations的原理就也知道了,这里就是订阅,对应的commit就是发布。
// 安装action
let actions = rawModule._raw.actions;
if (actions) {
forEach(actions, (actionName, value) => {
let arr = store.actions[namespace + actionName] || (store.actions[namespace + actionName] = []);
arr.push((payload) => {
value(store, payload);
});
});
}
复制代码
同理,依旧是订阅和发布。
动态注册是Vuex提供的一个类方法registerModule
// 动态注册
registerModule(moduleName, module) {
if (!Array.isArray(moduleName)) {
moduleName = [moduleName];
}
this.modules.register(moduleName, module); // 这里只作了格式化
installModule(this, this.state, moduleName, module.rawModule);// 从当前模块开始安装
}
复制代码
它有两个参数,第一个是要注册的模块名称,这个参数对应的register方法中的path,应该是一个数组类型。第二个是对应的选项。内部仍是先格式化而后安装的流程,安装时就用到了上面提到的双向绑定
rootModule.rawModule = rawModule;
复制代码
对应的
rawModule._raw = rootModule;
复制代码
rootModule是格式化以前的模块,rawModule是格式化以后的模块。rootModule
注意在安装的时候是从当前的模块开始的,并非从根模块开始。
使用方式:
注册一个单一模块
store.registerModule('d', {
state: {
age: 'd100'
}
});
复制代码
注册一个有层级的模块
store.registerModule(['b','c','e'], {
state: {
age: 'e100'
}
});
复制代码
多模块下,若是不使用命名空间为何不可以重名?
这个上面说getters实现原理时就提到过,由于安装时,不管是getters仍是mutations、actions, 要么是在一个对象中铺平,要么是在一个数组中铺平,若是重名且不使用命名空间势必会冲突。
用户选项中除了state、getters、mutations、actions等以外还有一个plugins选项,为每个mutation暴露一个钩子,结合Vuex提供的subscribe方法就可以监听到每一次的mutation信息。
插件其实就是函数,当new Vuex.Store() 时就会去执行一次
this.subs = [];
let plugins = options.plugins;
plugins.forEach(plugin => plugin(this));
复制代码
内部其实仍是发布和订阅的应用,这里咱们实现两个经常使用插件
在此以前咱们先看下Vuex提供的subscribe方法, 这是一个类方法
subscribe(fn) {
this.subs.push(fn);
}
复制代码
能够看到就是订阅,既然是用来监听mutation变化,那发布的位置必然是和mutation相关的,接下来咱们更改下mutation的安装
// 安装mutation
let mutations = rawModule._raw.mutations;
if (mutations) {
forEach(mutations, (mutationName, value) => {
let arr = store.mutations[namespace + mutationName] || (store. mutations[namespace + mutationName] = []);
arr.push((payload) => {
value(getState(store, path), payload);
store.subs.forEach(fn => fn({type: namespace + mutationName, payload: payload}, store.state)); // 这就用到了切片
});
});
}
复制代码
当执行了mutation以后,再去执行subs里面的每个方法,这里就是发布了。在这里咱们也看到了另一个编程的亮点:切片
,
arr.push((payload) => {
value(getState(store, path), payload);
store.subs.forEach(fn => fn({type: namespace + mutationName, payload: payload}, store.state)); // 这就用到了切片
});
复制代码
这里若是咱们不考虑subscribe,彻底就能够写成
arr.push(value);
复制代码
由于value就是一个mutation,是一个方法,直接存起来就行了,可是这样一来就无法作其余事情了,而使用切片就很方便咱们扩展,这就是切片编程的魅力所在。
下面咱们就看看logger插件的实现
function logger(store) {
let prevState = JSON.stringify(store.state);// 默认状态, 须要用JSON.stringify作一层深拷贝,不然preState是引用类型,那么当store.state变化,preState立马就跟着变化,这样就没法打印出上一次的状态
// let prevState = store.state;// 默认状态
// 订阅
store.subscribe((mutation, newState) => {// 每次调用mutation 此方法就会执行
console.log(prevState);
console.log(mutation);
console.log(JSON.stringify(newState));
prevState = JSON.stringify(newState);// 保存最新状态
});
}
复制代码
数据持久化
function persists(store) {
let local = localStorage.getItem('VUEX:state');
if (local) {
store.replaceState(JSON.parse(local));// 会用local替换全部状态
}
store.subscribe((mutation, state) => {
// 屡次更改只记录一次, 须要作一个防抖
debounce(() => {
localStorage.setItem('VUEX:state', JSON.stringify(state));
}, 500)();
});
}
复制代码
原理就是使用了浏览器自带的一个api localStorage,这里有一个新方法replaceState, 这也是一个类方法
replaceState(newState) {
this.vm.state = newState;
}
复制代码
该方法用来更新状态, 这里须要注意,咱们更改的仅仅是状态state, 而getters、 mutations、 actions执行时仍旧使用的是旧的状态,这个是在安装时决定的,所以咱们还须要让他们在每次执行的时候可以拿到最新的状态,因此还记得上面说的getState方法吗?
这里作了一个小的优化就是用节流作了一个优化, 以应对接二连三的更改状态,至于节流就再也不这赘述了。
最后咱们说下插件的使用:
plugins: [
persists,
logger
],
复制代码
注意logger方法, 第一次状态改变时,prev state只包含非动态注册的模块, next state包含全部模块,这是由于第一次执行logger方法的时候传入的store尚未包含动态注册的模块。
Vuex提供了mapState、mapGetters、mapMutations、mapActions这几个辅助方法,方便咱们书写。咱们把这几个分红两类:
computed: {
...mapState(['age']),// 解构处理
...mapGetters(['myAge'])
/* age() { return this.$store.state.age;// 和mapState效果同样 } */
},
复制代码
methods: {
// ...mapMutations(['syncChange']),
...mapMutations({aaa: 'syncChange'}),// 使用别名
...mapActions({bbb: 'asyncChange'})
}
复制代码
mapState
export function mapState (stateArr) {// {age: fn}
let obj = {};
stateArr.forEach(stateName => {
obj[stateName] = function() {
return this.$store.state[stateName];
}
});
return obj;
}
复制代码
mapGetters
export function mapGetters(gettersArr) {
let obj = {};
gettersArr.forEach(getterName => {
obj[getterName] = function() {
return this.$store.getters[getterName];
}
});
return obj;
}
复制代码
mapMutations
export function mapMutations(obj) {
let res = {};
Object.entries(obj).forEach(([key, value]) => {
res[key] = function(...args) {
this.$store.commit(value, ...args);
}
});
return res;
}
复制代码
mapActions
export function mapActions(obj) {
let res = {};
Object.entries(obj).forEach(([key, value]) => {
res[key] = function(...args) {
this.$store.dispatch(value, ...args);
}
});
return res;
}
复制代码
咱们这里分别实现了传入数组和对象类型的参数,源码中使用normalizeMap方法兼容了两者。最终返回了一个对象,所以咱们使用时须要进行解构。
let Vue;
let forEach = (obj, callback) => {
Object.keys(obj).forEach(key => {
callback(key, obj[key]);
})
}
class ModuleCollection {
constructor(options) {
// 深度遍历,将全部的子模块都遍历一遍
this.register([], options);
}
register(path, rootModule) {
let rawModule = {
_raw: rootModule,// 用户传入的模块定义
_children: {},
state: rootModule.state
}
rootModule.rawModule = rawModule;// 双向记录
if (!this.root) {
this.root = rawModule;
} else {
// 经典方法 找到当前模块的父亲
let parentModule = path.slice(0, -1).reduce((root, current) => { // 注册c的时候 [b, c].slice(0, -1) 至关于 [b, c].slice(0, 1) 的结果就是[b]
return root._children[current]
}, this.root);
parentModule._children[path[path.length-1]] = rawModule;
}
if (rootModule.modules) {
forEach(rootModule.modules, (moduleName, module) => {
// 注册a, [a] a模块的定义
// 注册b, [b] b模块的定义
// 注册c, [b, c] c模块的定义
this.register(path.concat(moduleName), module);
});
}
}
}
// 递归的获取每个模块的最新状态
function getState(store, path) {
let local = path.reduce((newState, current) => {
return newState[current];
}, store.state);
return local;
}
function installModule(store, rootState, path, rawModule) {// 安装时用的rawModule是格式化后的数据
// 安装子模块的状态
// 根据当前用户传入的配置 判断是否添加前缀
let root = store.modules.root // 获取到最终整个的格式化结果
let namespace = path.reduce((str, current) => {
root= root._children[current];// a
str = str + (root._raw.namespaced ? current + '/' : '');
return str;
}, '');
// console.log(path, namespace);
if(path.length > 0) {// 代表是子模块
// 若是是c,就先找到b
// [b,c,e] => [b, c] => c
let parentState = path.slice(0, -1).reduce((rootState, current) => {
return rootState[current];
}, rootState);
// vue的响应式不能对不存在的属性进行响应化
Vue.set(parentState, path[path.length-1], rawModule.state);
}
// 安装getters
let getters = rawModule._raw.getters;
if (getters) {
forEach(getters, (getterName, value) => {
Object.defineProperty(store.getters, namespace + getterName, {
get: () => {
// return value(rawModule.state);// rawModule就是当前的模块
return value(getState(store, path));
}
});
});
}
// 安装mutation
let mutations = rawModule._raw.mutations;
if (mutations) {
forEach(mutations, (mutationName, value) => {
let arr = store.mutations[namespace + mutationName] || (store. mutations[namespace + mutationName] = []);
arr.push((payload) => {
// value(rawModule.state, payload);// 真正执行mutation的地方
value(getState(store, path), payload);
store.subs.forEach(fn => fn({type: namespace + mutationName, payload: payload}, store.state)); // 这就用到了切片
});
});
}
// 安装action
let actions = rawModule._raw.actions;
if (actions) {
forEach(actions, (actionName, value) => {
let arr = store.actions[namespace + actionName] || (store.actions[namespace + actionName] = []);
arr.push((payload) => {
value(store, payload);
});
});
}
// 处理子模块
forEach(rawModule._children, (moduleName, rawModule) => {
installModule(store, rootState, path.concat(moduleName), rawModule)
});
}
class Store {
constructor(options) {
// console.log(options);
// 获取用户传入的全部属性
// this.state = options.state;
this.vm = new Vue({
data: {
state: options.state// 响应化处理
}
});
this.getters = {};// store内部使用的getters
this.mutations = {};
this.actions = {};
// 1. 须要将用户传入的对象格式化操做
this.modules = new ModuleCollection(options);
// 2. 递归安装模块 ,从根模块开始
installModule(this, this.state, [], this.modules.root);
this.subs = [];
let plugins = options.plugins;
plugins.forEach(plugin => plugin(this));
}
subscribe(fn) {
this.subs.push(fn);// 能够屡次订阅
}
replaceState(newState) {
this.vm.state = newState;// 更新状态, 这里须要注意,咱们更改的仅仅是状态state, 而getters mutations actions仍旧使用的时旧的状态,这个是在安装时决定的,所以咱们还须要让他们在每次执行的时候可以拿到最新的状态
}
get state() {// 获取实例上的state属性就会执行此方法
return this.vm.state
}
commit = (mutationName, payload) => {// es7写法, 这个里面的this永远指向的就是当前store实例
// this.mutations[mutationName](payload);
this.mutations[mutationName].forEach(mutation => mutation(payload));
}
dispatch = (actionName, payload) => {
// this.actions[actionName](payload);
this.actions[actionName].forEach(action => action(payload));
}
// 动态注册
registerModule(moduleName, module) {
if (!Array.isArray(moduleName)) {
moduleName = [moduleName];
}
this.modules.register(moduleName, module); // 这里只作了格式化
installModule(this, this.state, moduleName, module.rawModule);// 从当前模块开始安装
}
}
const install = (_Vue) => {
Vue = _Vue;
// 放到原型上不对,由于默认会把全部Vue实例都添加$store属性
// 咱们想要的是只从当前的根实例开始,到他全部的子组件都有$store属性
Vue.mixin({
beforeCreate() {
// console.log('这是mixin中的1', this.$options.name);
// 把根实例的store属性放到每个组件中
if (this.$options.store) {
this.$store = this.$options.store;
} else {
this.$store = this.$parent && this.$parent.$store;
}
}
});// 抽离公共的逻辑
}
export function mapState (stateArr) {// {age: fn}
let obj = {};
stateArr.forEach(stateName => {
obj[stateName] = function() {
return this.$store.state[stateName];
}
});
return obj;
}
export function mapGetters(gettersArr) {
let obj = {};
gettersArr.forEach(getterName => {
obj[getterName] = function() {
return this.$store.getters[getterName];
}
});
return obj;
}
export function mapMutations(obj) {
let res = {};
Object.entries(obj).forEach(([key, value]) => {
res[key] = function(...args) {
this.$store.commit(value, ...args);
}
});
return res;
}
export function mapActions(obj) {
let res = {};
Object.entries(obj).forEach(([key, value]) => {
res[key] = function(...args) {
this.$store.dispatch(value, ...args);
}
});
return res;
}
export default {
install,
Store
}
复制代码