vuex是一个专门为vue.js设计的集中式状态管理架构。我把他理解为在data中的属性需要共享给其他vue组件使用的部分。
vuex插件导出一个对象,包含install方法、Store类。在使用时第一步先install安装vuex插件,第二步配置options实例化Store类,并导出此store实例,将store实例添加到根组件的选项中。
install方法到底做了什么?
vuex插件中的install方法,做了很简单一件事,就是混入(mixin)vue生命周期的beforeCreate钩子函数。
beforeCreate钩子函数中做了什么?也很简单就是将注册到根组件中的store添加到所有的子组件中,使得所有的子组件可以共享这一个store实例(这也就是所谓的集中式状态管理)
Vue.mixin({
beforeCreate: vuexInit
});
function vuexInit() {
const options = this.$options;
// 使得所有所有vue实例都共享 $store 这个对象
if (options.store) {
// 根实例
this.$store = options.store;
} else if (options.parent && options.parent.$store) {
// 子组件
this.$store = options.parent.$store;
}
}
Store类的实现
在说实现之前,我们看一下Store的options都包含哪一些我们常用的。
export default new Vuex.Store({
strict: true, // 使用严格模式 只能通过mutation改数据,且mutation只能是同步代码
// 使用插件
plugins: [persists, logger()],
// 组件的状态 类似于 data
state: {
age: 10,
num: 1
},
// 获取 计算属性 computed
getters: {
getAge(state) {
// 需要传入 state
return state.age + 10;
}
},
// vue中的方法 唯一可以改变状态方法
// 同步的
mutations: {
// payload 是载荷的意思
changeAge(state, payload) {
state.age += payload;
}
},
// 异步修改state,当中也会调用mutations的同步方法
actions: {
asyncChangeAge({ commit }, payload) {
setTimeout(() => {
// changeAge是mutations中的方法
commit("changeAge", payload);
}, 1000);
}
},
modules: {
a: {
namespaced: true,
state: {
m_a_age: 20
},
mutations: {
changeAge(state) {
console.log("a子模块中的changeAge触发!");
return (state.m_a_age += 2);
}
}
},
b: {
namespaced: true,
state: {
m_b_age: 30
},
getters: {
getD(state) {
return state.m_b_age + 10;
}
},
mutations: {
changeAge(state) {
console.log("b子模块中的changeAge触发!");
return (state.m_b_age += 5);
}
},
modules: {
c: {
namespaced: true,
state: {
m_b_c_age: 40
},
mutations: {
changeAge(state) {
console.log("子模块b>c中的changeAge触发!");
return (state.m_b_c_age += 7);
}
}
}
}
},
}
});
/*
1.默认模块没有 作用域问题,如果需要作用域需要 设置namespaced:true
2.状态不要和模块的名字相同,如果相同 模块优先,会覆盖状态
原本想取值$store.state.b.m_b_age ,此时如果b的modules 中有一个子模块 也叫 m_b_age 那么就会优先区这个歌子模块,而取不到这个state
3.默认计算属性 直接通过getters取值 不需要添加模块
$store.getters.b.getD 这个是错误的
$store.getters.getD 这是正确的
但是如果该模块或该模块的父级配置了namespaced 就需要如下取值:
b子模块中的getters获取:{{ $store.getters["b/getD"] }}
4.默认会找当前模块上是否有namespace,并且将父级的namespace 一同算上做成命名空间
例如:
c模块本身有namespaced:true 父模块b本身也有namespaced:true 则在调用时,需要调用'b/c/属性'
c模块本身没有,父模块有,则在调用时,需要调用'b/属性'
c模块本身有,父模块没有,则在调用时,需要调用'c/属性'
*/
常用的options主要有:
-
strict
在严格模式下,state的改变只能使用mutations,且mutauions只能是同步
-
plugins
配置相关插件,在实例化store时会调用插件列表
-
namespaced
配置命名空间
-
state
与其他组件共享的状态
-
getters
类似于计算属性
-
mutations
同步方式,唯一可以修改状态的API,用户通过调用commit触发,这是基于订阅发布的方式
-
actions
异步方式,actions里面放一些异步逻辑用来修改状态,用户通过调用dispatch触发,在action内部也会调用commit,这也是基于订阅发布的方式(流程:用户调用dispatch触发对应action,执行完异步逻辑后,action会调用commit触发对应mutation修改数据,所以mutation是唯一的修改入口)
-
modules
可以在根模块下,配置相应的子模块
一、格式化options选项
将用户传入的options实例化为一个ModuleCollection对象
// 格式化 用户传入的参数,格式化成树形结构 更直观一些,后续也更好操作一些
this._modules = new ModuleCollection(options);
class ModuleCollection {
constructor(options) {
// 注册模块 第一个参数是一个[]数组,第二个参数是用户传入的options
this.register([], options);
}
register(path, rootModule) {
// 第一次register时 ,path =[] ,rootModule = options 就是用户传入的选项 实例化Module对象
// 实例化Module对象后,此时newModule对象就拥有 _rawModule(用户的原始模块配置)_children(子模块)state(当前模块的共享状态)
let newModule = new Module(rootModule);
// 在原始用户配置添加 newModule属性,值为对应的module对象
rootModule.newModule = newModule;
// 最开始 path是一个[]
if (path.length === 0) {
// 将此newModule 赋值给 this.root 此时的this是ModuleCollection的实例
this.root = newModule;
} else {
// 第二次调用时 此时path=["a"] path.slice(0, -1)后就是一个空数组,所以直接返回根module
// 查找父亲 this.root就是根module 第一次memo值为根module
let parent = path.slice(0, -1).reduce((memo, current) => {
return memo.getChild(current);
}, this.root);
// 此时 this.root中的_children中就有一个a module
// 此时 this.root中的_children中就有一个b module
// 第三次递归的时候 ,parent 就是this.root._children[b] ,所以会将c module添加到 this.root._children[b]中
// 在父亲的_children属性中添加newModule
parent.addChild([path[path.length - 1]], newModule);
}
// 如果rootModule中还有 modules属性,说明此模块有子模块,需要递归
if (rootModule.modules) {
// forEach是自定义的方法 forEach(对象,fn(value,key))
forEach(rootModule.modules, (module, moduleName) => {
// 这里的module是用户配置的子模块 , moduleName是配置的模块名
// 递归调用 第一个参数数组,始终保存着当前module的父级,可以这么理解
// 按照以上options,第一次调用时 path为[],moduleName为‘a’,module是用户的手动配置
this.register([...path, moduleName], module);
});
}
}
// 递归结束后,会形成以下树形结构:并赋值给this._modules 注意这里的this指的是store对象
// this.root = {
// _raw: xxx, // _raw 是指 原始的 用户配置
// state: xxx.state,
// _children: {
// a: {
// _raw: yyy,
// state: yyy.state
// },
// b: {
// _raw: ooo,
// state: ooo.state,
// _children: {
// c: {
// _raw: sss,
// state: sss.state
// }
// }
// }
// }
// };
class Module {
constructor(rootModule) {
// 接收传入的rootModule ,第一次是 options 也是根模块,后面全是子模块,赋值给module对象的_rawModule属性
this._rawModule = rootModule;
// 初始化_children属性为一个空对象
this._children = {};
// 将用户传入的模块state赋值给module对象的state属性,方便获取
this.state = rootModule.state;
}
// 定义 获取孩子的方法,返回的也是module对象
getChild(key) {
return this._children[key];
}
// 定义 添加孩子的方法
addChild(key, Module) {
this._children[key] = Module;
}
...
}
二、将格式化后的modules安装到store中
这个阶段的目的是,将modules添加到store的属性上(可以这么理解)。此时的modules是一个树形结构的对象,拥有_rawModule、__children、state属性。
installModule()方法调用结束后,此时 this._mutations 、 this._actions 、 this._wrappedGetters中都会有数据了,且根state中也保存子state。
constructor(options) {
// ...
let state = this._modules.root.state; // 根的状态
this._mutations = {}; // 存放所有模块中的mutation
this._actions = {}; // 存放所有模块中的actions
this._wrappedGetters = {}; // 存放所有模块中的getters
// 第一个参数 this:当前store实例
// 第二个参数 state:根state
// 第三个参数 []
// 第四个参数 根module
installModule(this, state, [], this._modules.root);
}
// 第一个参数 传入的store实例
// 第二个参数 根state
// 第三个参数 path=[]
// 第四个参数 根module
function installModule(store, rootState, path, module) {
// getNameSpace返回的是类似于 ‘a/b/c/’的字符串
let namespaced = store._modules.getNameSpace(path);
// 如果path的长度大于0,说明根模块有child
// path = ["a"] 深度 1级
// path = ['b','c'] 深度 2级
// path = ['d','e','f'] 深度 3级
if (path.length > 0) {
let parent = path.slice(0, -1).reduce((memo, current) => {
// 第一次是[a],然后 被slice(0,-1)截取后是一个[]数组,直接返回根的state并赋值给parent变量,所以会在根的state中添加a属性,值为a模块的state
// 第二次是[b],然后 被slice(0,-1)截取后是一个[]数组,直接返回根的state并赋值给parent变量,所以会在根的state中添加b属性,值为b模块的state
// 第三次是[b,c]然后 被slice(0,-1)截取后[b],此时返回state.b并赋值给parent变量,然后在b模块的state中添加c属性,值为c模块的state
return memo[current];
}, rootState);
// _withCommitting 的作用是确保 用户是mutation调用的,与strict有关,后面会说,主要关注 Vue.set
store._withCommitting(() => {
// parent 在深度一级的情况下就 根state
Vue.set(parent, path[path.length - 1], module.state);
});
// child遍历结束后,rootState中会有所有子模块的state 需要通过. 的方式去获取
// 例如 :$store.state.属性名 $store.state.模块名.属性名
}
// 下面的forEachMutation、forEachAction、forEachGetters、forEachChild都是在module中定义的,查看第二段代码
module.forEachMutation((mutation, type) => {
// 先看 store._mutations是一个对象,键 为 namespaced + type ,值为数组,数组里面是一个个mutation
// 所以如果不定义namespaced的话,所有模块的mutation都会被放在一起,然后会一起被调用
store._mutations[namespaced + type] =
store._mutations[namespaced + type] || [];
// 将所有模块的mutation外面再包装一个函数并 类似于函数切片 添加到 store._mutataions中
// 将当前模块的mutations都订阅到store._mutations[namespaced + type]数组中,等用户commit时依次触发
store._mutations[namespaced + type].push(payload => {
// 内部可能会替换新的状态(replaceState方法 暂时还没有说道),这里如果一直使用module.state 可能是老的状态 闭包的问题,这里的module是installMdoule参数中传入的module 所以使用getState(store, path) 是获取最新的state
store._withCommitting(() => {
mutation.call(store, getState(store, path), payload);
});
// 先不用看,谢谢
store._subscribers.forEach(sub => {
return sub({ mutation, type }, store.state);
});
});
});
module.forEachAction((action, type) => {
// 类似于 mutations
store._actions[namespaced + type] = store._actions[namespaced + type] || [];
store._actions[namespaced + type].push(payload => {
action.call(store, store, payload);
});
});
module.forEachGetters((getter, key) => {
// 注意 同名的会被覆盖,因为getter 会被放在同一个对象中
store._wrappedGetters[namespaced + key] = function() {
// getState()获取最新的state
return getter(getState(store, path));
};
});
module.forEachChild((child, key) => {
// 如果有孩子就继续递归
// store 还是原来的stroe 没有改变
// rootState 还是原来的根state
// 如果 将子module的key添加到path中 ,child是子module
installModule(store, rootState, path.concat(key), child);
});
}
class ModuleCollection {
// ...
getNameSpace(path) {
// 获取根module
let root = this.root;
return path.reduce((namespace, key) => {
// 如果 path =['a','b','c']
// root = root._children['a'] 然后判断有没有 nameSpced (注意这里的nameSpaced是不是用户写的,是Module类中的属性访问器,只不过恰巧同名了),有的话 就拼接 ‘a/’
// root = root._children['a']._children['b'] 然后判断有没有 nameSpced 有的话 就拼接 ‘a/b/’
// root = root._children['a']._children['b']._children['c'] 然后判断有没有 nameSpced 有的话 就拼接 ‘a/b/c/’
// 所以 getNameSpace方法最后返回的是 类似于‘a/b/c/’的字符串
root = root.getChild(key);
return namespace + (root.nameSpaced ? key + "/" : "");
}, "");
}
}
class Module {
// ...
forEachMutation(fn) {
if (this._rawModule.mutations) {
forEach(this._rawModule.mutations, fn);
}
}
forEachAction(fn) {
if (this._rawModule.actions) {
forEach(this._rawModule.actions, fn);
}
}
forEachGetters(fn) {
if (this._rawModule.getters) {
forEach(this._rawModule.getters, fn);
}
}
forEachChild(fn) {
forEach(this._children, fn);
}
}
三、状态转换响应式
经过第二个步骤后,此时所有的状态都已经存在于state属性中了,就下来我们就需要将state转换为响应式的。
class Store {
constructor(options) {
// ...
// this是当前store实例 ,state是根state
resetStoreVm(this, state);
// ...
}
}
function resetStoreVm(store, state) {
// store._vm一开始是没有的,这里保存 oldVm 是在调用repalceState方法时,需要销毁旧的vm实例
let oldVm = store._vm;
// 第一步获取 getters的集合对象
let _wrappedGetters = store._wrappedGetters;
// 第二步 定义computed对象 等下添加到vm中 计算属性
let computed = {};
// 在 store实例上,定义一个getters属性 用于用户使用
store.getters = {};
// 遍历 _wrappedGetters对象,并定义到computed中
forEach(_wrappedGetters, (fn, key) => {
computed[key] = function() {
return fn(store.state);
};
// 并在store.getters中定义
Object.defineProperty(store.getters, key, {
get: () => store._vm[key]
});
});
store._vm = new Vue({
data() {
return {
$$state: state
};
},
computed
});
// 如果用户使用了 strict 严格模式,需要对 store._vm._data.$$state 深度监听,且同步执行
if (store.strict) {
store._vm.$watch(
// 必须函数返回一个对象
() => store._vm._data.$$state,
() => {
// 只要状态一变化就会立即执行
// console.assert(exp,msg) exp必须返回boolean 在exp为false时,输出 msg
console.assert(store._committing, "在mutation外改变了状态");
},
{
deep: true,
sync: true // 同步执行
}
);
}
// 如果存在oldVm则进行销毁
if (oldVm) {
Vue.nextTick(() => {
oldVm.$destroyed();
});
}
}
至此的话,Store类的constructor都执行完毕了。
_withCommitting是干嘛的?
通俗点说,就是在调用mutation修改state之前,将store._committing属性设置为true,然后在mutation执行完毕后,再将stroe._committing设置为false。
为什么要这要做呢?因为用户用户设置了strict:true,在严格模式下,只能通过mutation去修改属性,且mutation是同步。不能通过watch观察根state,只要观察到根state有变化,就会执行回调函数
() => {
// 只要状态一变化就会立即执行
// console.assert(exp,msg) exp必须返回boolean 在exp为false时,输出 msg
console.assert(store._committing, "在mutation外改变了状态");
},
subscribe订阅
将fn订阅到store._subscribers中,每当commit时,会执行对应的mutation然后依次调用store._subscribers中的fn。
注意:一定是通过commit的方式触发的mutation才会执行fn
replaceState替换State
为什么会替换配置好的State?可以用来持久化本地存储。
replaceState(newState) {
this._withCommitting(() => {
// 一定要用_withCommitting包裹,不然在严格模式下会报错
// 数据改变,会触发当前组件实例视图更新
this._vm._data.$$state = newState;
});
}
registerModule动态注册模块
registerModule(path, rawModule) {
// 将path转换为数组
if (typeof path == "string") {
path = [path];
}
// 模块注册 ['u'] 将u注册到根module下
this._modules.register(path, rawModule);
// 模块安装
installModule(this, this.state, path, rawModule.newModule);
// 重新定义getters
resetStoreVm(this, this.state);
}