前言
再上一篇文章《深入浅出VueX》中,我们简单的实现了一个 VueX ,并能够正常使用,但是没有实现其模块化思想,另外,命名空间、辅助函数、数据持久化的插件都没有实现,今天,我们来重新编写我们的源代码,实现一个功能更丰富的VueX 实现全局数据管理。
模块化的实现方式
实现模块化需要用到多叉树的数据结构,我们通过一个例子来说明这个:
let store = new Vuex.Store({
state: { count: localStorage.getItem("qqq") / 1 || 100 },
getters: {
myAge(state) {
return state.count + 20;
},
},
mutations: {
...
},
actions: {
...
},
modules: {
a: {
state: { age: 200 },
mutations: {
...
},
modules: {
a1: {
namespaced: true,
state: { age: 300 },
mutations: {
...
},
},
},
},
b: {
namespaced: true,
state: { age: 400 },
mutations: {
...
},
},
},
});
在这个store配置项中,主模块的modules包含a和b两个子模块,a子模块包含a1子模块。我们需要将它转化成以下形式,再来进行操作:
this.root = {
_raw: '根模块',
_children:{
a:{
_raw:"a模块",
_children:{
a1:{
.....
}
},
state:'a的状态'
},
b:{
_raw:"b模块",
_children:{},
state:'b的状态'
}
},
state:'根模块自己的状态'
}
编写 VueX
从这里开始我们自己来实现一个 VueX 插件,这里先来标注一下整体目录结构:
├─vuex
│ ├─module # 数据的格式化 格式化成树结构
│ │ ├─module.js
│ │ └─module-collection.js
│ ├─helpers.js # 创建辅助函数 mapState、mapGetters等等
│ ├─mixin.js # 将根组件中注入的store 分派给每一个子组件
│ ├─plugin.js # 持久化等等的插件
│ ├─index.js # 引用时的入口文件
│ ├─store.js # store构造类和install方法
│ ├─util.js # forEachValue函数,遍历对象
默认我们引用VueX使用的是index.js文件,use方法默认会调用当前返回对象的install方法。所以这个文件是入口文件,核心就是导出所有写好的方法。
import { Store, install } from "./store.js";
// Vuex.Store Vuex.install
export default {
Store,
install,
};
export * from "./helpers";
export * from "./util";
在store.js文件里实现install方法
import applyMixin from "./mixin";
// Vue.use 方法会调用插件的install方法,此方法中的参数就是Vue的构造函数
// Vue.use = function (plugin) {
// plugin.install(this);
// }
export const install = (_Vue) => {
// 插件的安装 Vue.use(Vuex)
// _Vue 是Vue的构造函数
Vue = _Vue;
// 需要将根组件中注入的store 分派给每一个组件 (子组件) Vue.mixin
applyMixin(Vue);
};
在 mixin.js 文件里实现 applyMixin 方法
export default function applyMixin(Vue) {
// 父子组件的beforeCreate执行顺序
Vue.mixin({
// 内部会把生命周期函数 拍平成一个数组
beforeCreate: vuexInit,
});
}
// 组件渲染时从父=》子
function vuexInit() {
// 给所有的组件增加$store 属性 指向我们创建的store实例
const options = this.$options; // 获取用户所有的选项
if (options.store) {
// 根实例
this.$store = options.store;
} else if (options.parent && options.parent.$store) {
// 儿子 或者孙子....
this.$store = options.parent.$store;
}
}
在util.js里面实现forEachValue方法。
// 遍历对象,传入一个回调函数对对象中的每一项进行操作
export const forEachValue = (obj, callback) => {
Object.keys(obj).forEach((key) => callback(obj[key], key));
};
在store.js文件里实现 Store 构造函数
import applyMixin from "./mixin";
import { forEachValue } from "./util";
import ModuleCollection from "./module/module-collection";
export let Vue;
// 容器的初始化
export class Store {
constructor(options) {
// options 就是你new Vuex.Store({state,mutation,actions})
const state = options.state; // 数据变化要更新视图 (vue的核心逻辑依赖收集)
this._mutations = {};
this._actions = {};
this._wrappedGetters = {};
this._subscribes = [];
this.getters = null;
this._vm = null;
this.commit = null;
this.dispatch = null;
// 数据的格式化 格式化成我想要的结果 (树)
// 1.模块收集
this._modules = new ModuleCollection(options);
// 根模块的状态中 要将子模块通过模块名 定义在根模块上
// 2.安装模块
installModule(this, state, [], this._modules.root);
// 3,将状态和getters 都定义在当前的vm上
resetStoreVM(this, state);
// 插件内部会依次执行
options.plugins && options.plugins.forEach((plugin) => plugin(this));
// 实现commit方法
this.commit = (type, payload) => {
//保证当前this 当前store实例
// 调用commit其实就是去找 刚才绑定的好的mutation
this._mutations[type].forEach((mutation) => mutation(payload));
};
// 实现dispatch方法
this.dispatch = (type, payload) => {
this._actions[type].forEach((action) => action.call(this, payload));
};
}
replaceState(state) {
// 替换掉最新的状态
this._vm._data.$$state = state;
}
subscribe(fn) {
this._subscribes.push(fn);
}
get state() {
// 属性访问器 new Store().state Object.defineProperty({get()})
return this._vm._data.$$state;
}
}
在module目录下的module-collection.js里面进行数据的格式化。
import { forEachValue } from "../util";
import Module from "./module";
class ModuleCollection {
constructor(options) {
this.root = null;
this.register([], options);
}
register(path, rootModule) {
// [a, c] [b]
let newModule = new Module(rootModule);
if (!this.root) {
// 根模块
this.root = newModule; // this.root就是树根
console.log(this.root);
} else {
// reduce循环递归拿到上级目录,并把当前目录作为子节点放置到上级目录的_children属性上
// 不过要截取数组最后一项,因为要根据最后一项赋值。
let parent = path.slice(0, -1).reduce((memo, current) => {
return memo.getChild(current);
}, this.root);
// 通过forEach遍历也一样。
// let parent = this.root;
// path.slice(0, -1).forEach((item) => {
// parent = parent.getChild(item);
// });
parent.addChild(path[path.length - 1], newModule);
}
if (rootModule.modules) {
// 循环模块 [a] [b]
forEachValue(rootModule.modules, (module, moduleName) => {
// [a,a1]
this.register(path.concat(moduleName), module);
});
}
}
getNamespaced(path) {
let root = this.root; // 从根模块找起来
return path.reduce((str, key) => {
// [a,a1]
root = root.getChild(key); // 不停的去找当前的模块
return str + (root.namespaced ? key + "/" : "");
}, ""); // 参数就是一个字符串
}
}
export default ModuleCollection;
在module目录下的module.js抽离模块类
import { forEachValue } from "../util";
class Module {
constructor(newModule) {
this._raw = newModule;
this._children = {};
this.state = newModule.state;
}
getChild(key) {
return this._children[key];
}
addChild(key, module) {
this._children[key] = module;
}
// 给模块继续扩展方法
forEachMutation(fn) {
if (this._raw.mutations) {
forEachValue(this._raw.mutations, fn);
}
}
forEachAction(fn) {
if (this._raw.actions) {
forEachValue(this._raw.actions, fn);
}
}
forEachGetter(fn) {
if (this._raw.getters) {
forEachValue(this._raw.getters, fn);
}
}
forEachChild(fn) {
forEachValue(this._children, fn);
}
get namespaced() {
return this._raw.namespaced;
}
}
export default Module;
在store.js里面完成installModule方法用以安装模块
通过在Module模块类中提供遍历方法,例如forEachMutation、forEachAction...对模块进行安装操作
/**
* @param store 容器
* @param rootState 根模块
* @param path 所有路径
* @param module 我们刚刚格式化后的结果
*/
const installModule = (store, rootState, path, module) => {
// 这里我要对当前模块进行操作
// 将所有的子模块的状态安装到父模块的状态上
// 给当前订阅的事件 增加一个命名空间
let namespace = store._modules.getNamespaced(path); // 返回前缀即可
// a/changeAge b/changeAge a/c/changeAge path[a] [b] [a,c]
if (path.length > 0) {
// vuex 可以动态的添加模块
let parent = path.slice(0, -1).reduce((memo, current) => {
return memo[current];
}, rootState);
// 如果这个对象本身不是响应式的 那么Vue.set 就相当于obj[属性 ]= 值
Vue.set(parent, path[path.length - 1], module.state);
}
module.forEachMutation((mutation, key) => {
store._mutations[namespace + key] = store._mutations[namespace + key] || [];
store._mutations[namespace + key].push((payload) => {
mutation.call(store, getState(store, path), payload);
store._subscribes.forEach((fn) => {
fn(mutation, store.state); // 用最新的状态
});
});
});
module.forEachAction((action, key) => {
store._actions[namespace + key] = store._actions[namespace + key] || [];
store._actions[namespace + key].push((payload) => {
action.call(store, store, payload);
});
});
module.forEachGetter((getter, key) => {
// 模块中getter的名字重复了会覆盖
store._wrappedGetters[namespace + key] = function() {
return getter(getState(store, path));
};
});
module.forEachChild((child, key) => {
// 递归加载模块
installModule(store, rootState, path.concat(key), child);
});
};
在Store构造函数内部完成commit和dispatch方法
// 实现commit方法
this.commit = (type, payload) => {
//保证当前this 当前store实例
// 调用commit其实就是去找 刚才绑定的好的mutation
this._mutations[type].forEach((mutation) => mutation(payload));
};
// 实现dispatch方法
this.dispatch = (type, payload) => {
this._actions[type].forEach((action) => action.call(this, payload));
};
通过resetStoreVM方法定义状态和计算属性
通过“resetStoreVM(this, state)”将状态和getters 都定义在当前的vm上
function resetStoreVM(store, state) {
const computed = {}; // 定义计算属性
store.getters = {}; // 定义store中的getters
forEachValue(store._wrappedGetters, (fn, key) => {
computed[key] = () => {
return fn();
};
Object.defineProperty(store.getters, key, {
get: () => store._vm[key], // 去计算属性中取值
});
});
store._vm = new Vue({
data: {
$$state: state,
},
computed, // 计算属性有缓存效果
});
}
命名空间的实现
在ModuleCollection和Module构造函数里面实现命名空间:
// ModuleCollection:
getNamespaced(path) {
let root = this.root; // 从根模块找起来
return path.reduce((str, key) => {
// [a,a1]
root = root.getChild(key); // 不停的去找当前的模块
return str + (root.namespaced ? key + "/" : "");
}, ""); // 参数就是一个字符串
}
// Module
get namespaced() {
return !!this._raw.namespaced;
}
在installModule方法里面绑定属性,增加命名空间即可,例如:
module.forEachMutation((mutation, key) => {
store._mutations[namespace + key] = (store._mutations[namespace + key] || []);
store._mutations[namespace + key].push((payload) => {
mutation.call(store, module.state, payload);
});
});
插件
在plugins.js里面实现持久化插件
export function persists() {
return function(store) {
// store是当前默认传递的
let data = localStorage.getItem("VUEX:STATE");
if (data) {
store.replaceState(JSON.parse(data));
}
store.subscribe((mutation, state) => {
localStorage.setItem("VUEX:STATE", JSON.stringify(state));
});
};
}
在Store类里面实现subscribe、replaceState方法
// 造constructor函数里面执行插件
options.plugins && options.plugins.forEach((plugin) => plugin(this));
// 实现两个插件
replaceState(state) {
// 替换掉最新的状态
this._vm._data.$$state = state;
}
subscribe(fn) {
this._subscribes.push(fn);
}
在store.js里面实现getState方法
function getState(store, path) {
// 获取最新的状态 可以保证视图更新
return path.reduce((newState, current) => {
return newState[current];
}, store.state);
}
获取最新状态
module.forEachMutation((mutation, key) => {
store._mutations[namespace + key] = store._mutations[namespace + key] || [];
store._mutations[namespace + key].push((payload) => {
mutation.call(store, getState(store, path), payload);
store._subscribes.forEach((fn) => {
fn(mutation, store.state); // 用最新的状态
});
});
});
辅助函数
在helper.js实现辅助函数
export function mapState(stateArr) {
let obj = {};
for (let i = 0; i < stateArr.length; i++) {
let stateName = stateArr[i];
obj[stateName] = function() {
return this.$store.state[stateName];
};
}
return obj;
}
export function mapGetters(gettersArr) {
let obj = {};
for (let i = 0; i < gettersArr.length; i++) {
let getterName = gettersArr[i];
obj[getterName] = function() {
return this.$store.getters[getterName];
};
}
return obj;
}
export function mapMutations(mutationList) {
let obj = {};
for (let i = 0; i < mutationList.length; i++) {
let type = mutationList[i];
obj[type] = function(payload) {
this.$store.commit(type, payload);
};
}
return obj;
}
export function mapActions(actionList) {
let obj = {};
for (let i = 0; i < actionList.length; i++) {
let type = actionList[i];
obj[type] = function(payload) {
this.$store.dispatch(type, payload);
};
}
return obj;
}
简单使用
store/index.js配置:
import Vue from "vue";
// import Vuex from "vuex";
import Vuex, { persists } from "../vuex/index.js";
Vue.use(Vuex);
let store = new Vuex.Store({
state: { count: localStorage.getItem("qqq") / 1 || 100 },
plugins: [
// logger(), // vuex-persists
persists(),
],
getters: {
myAge(state) {
return state.count + 20;
},
},
mutations: {
syncChange(state, payload) {
state.count += payload;
localStorage.setItem("qqq", state.count);
},
},
actions: {
changeCountFn(store, payload) {
setTimeout(() => {
store.commit("syncChange", payload);
}, 500);
},
},
modules: {
a: {
state: { age: 200 },
mutations: {
syncChange(state, payload) {
state.age = state.age + payload;
console.log(state.age);
},
},
modules: {
a1: {
namespaced: true,
state: { age: 300 },
mutations: {
syncChange(state, payload) {
state.age = state.age + payload;
console.log(state.age + payload);
},
},
},
},
},
b: {
namespaced: true,
state: { age: 400 },
mutations: {
syncChange(state, payload) {
state.age = state.age + payload;
console.log(state.age);
},
},
},
},
});
export default store;
bro1.vue配置:
<template>
<div class="bro1">
<h2>当前数字是{{count}}</h2>
<h2>当前getters是{{myAge}}</h2>
<h2>当前a模块是{{$store.state.a.age}}</h2>
<h2>当前a1模块是{{$store.state.a.a1.age}}</h2>
<h2>当前b模块是{{$store.state.b.age}}</h2>
</div>
</template>
<script>
import { mapState, mapGetters } from "../vuex";
export default {
name: "bro1",
methods: {},
computed: {
// count() {
// return this.$store.state.count;
// },
// myAge() {
// return this.$store.getters.myAge;
// },
...mapState(["count"]),
...mapGetters(["myAge"]),
},
};
</script>
<style lang="less" scoped>
.bro1 {
border: 1px solid #8616ee;
}
</style>
bro2.vue配置:
<template>
<div>
<h2>当前数字是{{$store.state.count}}</h2>
<button @click="add">+1</button>
<button @click="minus">-1</button>
</div>
</template>
<script>
export default {
name: "bro2",
methods: {
add() {
this.$store.dispatch("changeCountFn", 1);
this.$store.commit("a1/syncChange", 1);
},
minus() {
this.$store.commit("syncChange", -1);
this.$store.commit("a1/syncChange", -1);
},
},
mounted() {
console.log(this.$store);
},
};
</script>
App.vue配置:
<template>
<div id="myApp">
<h1>{{name}}</h1>
<bro1></bro1>
<bro2></bro2>
</div>
</template>
总结
VueX源码解析完毕,通过VueX 源码,我们可以更清楚的知道VueX的工作流程。方便工作和维护。