vuex 原理剖析
一.Vuex 基本使用及用法
vuex 是 vue 的状态管理工具,为了更方便实现多个组件共享状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// store/index.js
import Vue from 'vue';
// import Vuex from 'vuex'; // 原生
import Vuex from '../vuex'; // 手写
import a from './modules/a';
import b from './modules/b';
// Vue.use的操作目的是帮我们使用插件
// Vue.use = function (plugin, options) { // use方法默认会将Vue的构造函数传入到插件中 为了解决写插件的时候依赖Vue的问题
// if (typeof plugin === 'function') {
// return plugin(this, options);
// } else {
// plugin.install(this, options);
// }
// };
Vue.use(Vuex);
const persitsPlugin = (store) => {
let persists = localStorage.getItem('VUEX_PERSITS');
if (persists) {
store.replaceState(JSON.parse(persists));
}
store.subscribe((mutation, state) => {
localStorage.setItem('VUEX_PERSITS', JSON.stringify(state));
});
};
const Store = new Vuex.Store({
strict: true,
plugins: [persitsPlugin],
state: {
age: 12,
a: 100,
b: 200,
},
getters: {
myAge(state) {
return state.age + 20;
},
},
mutations: {
addAge(state, payload) {
state.age += payload;
},
},
actions: {
addAge({ commit }, payload) {
setTimeout(() => {
commit('addAge', payload);
}, 2000);
},
},
modules: {
a,
b,
},
});
setTimeout(() => {
Store.registerModule('cc', {
state: {
a: 66,
b: 88,
},
getters: {
myA(state) {
return state.a + 100;
},
},
});
}, 1000);
export default Store;
这里可以类比:state 类比为最贱的状态,getters 类比为组件的计算属性,mutation 类比为组件中的方法(可以更改组件的状态),actions 用于进行一步操作将结构提交给 mutation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// App.vue
<div id="app">
年龄:
我的年龄:
我的年龄:
我的年龄:
<button @click="$store.state.age+=33">修改年龄</button>
<button @click="$store.commit('b/addAge', 10)">直接mutation修改</button>
<button @click="$store.dispatch('addAge', 10)">action修改</button>
<button @click="$store.state.age+=100">错误的修改</button>
<!-- -->
()
</div>
这个$store 属性是通过根实例传入的
1
2
3
4
5
// main.js
new Vue({
store,
render: (h) => h(App), // 默认引用的是runtime-only 不包含将template -> render函数
}).$mount('#app');
内部会将 store 熟悉挂载在每个实例上命名为$store,这样所有组件都可以操作同一个 store 属性
二.自己实现 Vuex 模块
实现入口文件,默认导出 Store 类和 install 方法
1
2
3
4
5
6
7
8
9
10
// vuex/index.js
import { install } from './install';
import Store from './store';
const Vuex = {
Store, // 容器核心代码
install, // 将$store属性共享给每个组件 每个组件都可以获取到这个实例 插件的入库 mixin
};
export default Vuex;
3.1.install 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// vuex/install.js
export let Vue; // export导出的是一个变量 如果变量变化了导出的结果也会发生变化
export const install = (_Vue, options) => {
// 需要在所有组件里面都定义一个$store属性共享给每个组件 把根模块中注入的$store共享出去
Vue = _Vue;
Vue.mixin({
// 让所有的组件都能获取到$store实例 此实例找到的就是main.js中注入的实例
beforeCreate() {
const options = this.$options; // 获取用户的所有选项
if (options.store) {
// 给根实例增加$store属性
this.$store = options.store;
} else if (options.parent && options.parent.$store) {
// 给组件增加$store属性
this.$store = options.parent.$store;
}
},
});
};
当我们使用插件时默认会执行 install 方法并传入 Vue 的构造函数
3.2.实现 state
1
2
3
4
5
6
7
8
9
10
11
12
13
export class Store {
constructor(options) {
let state = options.state;
this._vm = new Vue({
data: {
$$state: state,
},
});
}
get state() {
return this._vm._data.$$state;
}
}
将用户传入的数据定义在 vue 的实例上(这个就是 vuex 核心)产生一个单独的 vue 实例进行通信,这里要注意的是定义$开头的变量不会被代理到实例上
3.3.实现 getters
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
this.getters = {};
const computed = {};
forEachValue(options.getters, (fn, key) => {
computed[key] = () => {
return fn(this.state);
};
Object.defineProperty(this.getters, key, {
get: () => this._vm[key],
});
});
this._vm = new Vue({
data: {
$$state: state,
},
computed, // 利用计算属性实现缓存
});
3.4.实现 mutations
1
2
3
4
5
6
7
8
9
10
11
export class Store {
constructor(options) {
this.mutations = {};
forEachValue(options.mutations, (fn, key) => {
this.mutations[key] = (payload) => fn.call(this, this.state, payload);
});
}
commit = (type, payload) => {
this.mutations[type](payload);
};
}
3.5.实现 actions
1
2
3
4
5
6
7
8
9
10
11
export class Store {
constructor(options) {
this.actions = {};
forEachValue(options.actions, (fn, key) => {
this.actions[key] = (payload) => fn.call(this, this, payload);
});
}
dispatch = (type, payload) => {
this.actions[type](payload);
};
}
三.实现模块机制
3.1.格式化用户数据
1
2
3
// vuex/store.js
import ModuleCollection from './module/module-collection';
this.modules = new ModuleCollection(options);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// vuex/module/module-collection.js
import { forEachValue } from '../utils';
import Module from './module';
class ModuleCollection {
// 构建父子关系用栈
constructor(options) {
this.register([], options); // 递归实现收集要记录父子关系
}
getNamespace(path) {
let module = this.root;
return path.reduce((namespace, key) => {
module = module.getChild(key);
console.log(module);
return namespace + (module.namespaced ? key + '/' : '');
}, '');
}
register(path, rootModule) {
let module = new Module(rootModule); // _raw _children state
rootModule.wrapperModule = module;
if (path.length == 0) {
this.root = module;
} else {
// ['a'] ['b']
// 不是根的情况
let parent = path.slice(0, -1).reduce((memo, current) => {
return memo.getChild(current);
}, this.root);
parent.addChild(path[path.length - 1], module);
}
if (rootModule.modules) {
// 获取到定义的子模块
forEachValue(rootModule.modules, (moduleName, moduleValue) => {
this.register(path.concat(moduleName), moduleValue);
});
}
}
}
export default ModuleCollection;
3.2.抽离模块类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// vuex/module/module.js
class Module {
constructor(rootModule) {
this._raw = rootModule;
this._children = {};
this.state = rootModule.state;
}
getChild(key) {
return this._children[key];
}
addChild(key, module) {
this._children[key] = module;
}
}
export default Module;
方便模块扩展
3.3.安装模块
1
2
3
4
5
6
7
8
// vuex/store.js
this.actions = {}; // 存放所有模块的action
this.mutations = {}; // 存放所有模块的mutation
// 将子的状态定义在父的状态上
this.wrapperGetters = {};
// 除了安装 mutation action getters 这些之外 最重要的是状态
const state = options.state;
installModule(this, [], this.modules.root, state); // state跟模块的状态对象
在模块类中提供遍历方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// vuex/module/module.js
import { forEachValue } from '../utils';
class Module {
constructor(rootModule) {
this._raw = rootModule;
this._children = {};
this.state = rootModule.state;
}
get namespaced() {
return !!this._raw.namespaced;
}
getChild(key) {
return this._children[key];
}
addChild(key, module) {
this._children[key] = module;
}
forEachMutation(cb) {
if (this._raw.mutations) {
forEachValue(this._raw.mutations, cb);
}
}
forEachAction(cb) {
if (this._raw.actions) {
forEachValue(this._raw.actions, cb);
}
}
forEachGetter(cb) {
if (this._raw.getters) {
forEachValue(this._raw.getters, cb);
}
}
forEachChildren(cb) {
forEachValue(this._children, cb);
}
}
export default Module;
对模块进行安装操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// vuex/store.js
function installModule(store, path, module, rootState) {
let namespaced = store.modules.getNamespace(path);
if (path.length > 0) {
// 是子模块 需要将数据处理到rootState上
let parent = path.slice(0, -1).reduce((memo, current) => {
return memo[current];
}, rootState);
// 添加一个不存在的属性需要使用Vue.set方法 否则无效
store.withCommitting(() => {
Vue.set(parent, path[path.length - 1], module.state);
});
}
// 将循环的方法放到类中来维护 Object.entries(module._raw.mutations)
module.forEachMutation((mutationKey, mutationValue) => {
store.mutations[namespaced + mutationKey] =
store.mutations[namespaced + mutationKey] || [];
store.mutations[namespaced + mutationKey].push((payload) => {
store.withCommitting(() => {
mutationValue(getState(store, path), payload);
});
store._subscribes.forEach((callback) =>
callback({ type: mutationKey }, store.state)
);
});
});
module.forEachAction((actionKey, actionValue) => {
store.actions[namespaced + actionKey] =
store.actions[namespaced + actionKey] || [];
store.actions[namespaced + actionKey].push((payload) => {
actionValue(store, payload);
});
});
module.forEachGetter((getterKey, getterValue) => {
// 应该先判断一下store.getters是否有这个属性 如果有就不必要再注册
if (store.wrapperGetters[namespaced + getterKey]) {
return console.error('duplicate key');
}
store.wrapperGetters[namespaced + getterKey] = () => {
return getterValue(getState(store, path));
};
});
// 将循环的方法放到类中来维护 Object.entries(module._children)
module.forEachChildren((childName, childValue) => {
installModule(store, path.concat(childName), childValue, rootState);
});
}
对dispatch
和action
方法进行重写
1
2
3
4
5
6
7
8
9
// vuex/store.js
dispatch = (actionKey, payload) => {
let actions = this.actions[actionKey];
actions && actions.forEach((fn) => fn(payload));
}; // this.actions
commit = (mutationKey, payload) => {
let mutations = this.mutations[mutationKey];
mutations && mutations.forEach((fn) => fn(payload));
}; // this.mutations
3.4.定义状态和计算属性
1
resetVM(this, state);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// vuex/store.js
function resetVM(store, state) {
let oldVm = store.vm; // 拿到以前的
const computed = {};
store.getters = {};
forEachValue(store.wrapperGetters, (getterKey, getterValue) => {
computed[getterKey] = () => getterValue(store.state);
Object.defineProperty(store.getters, getterKey, {
get: () => store.vm[getterKey], // vuex中的getters只有get
});
});
store.vm = new Vue({
data: {
$$state: state,
},
computed,
});
store.vm.$watch(
() => store.vm._data.$$state,
() => {
console.assert(store._commiting, 'outside mutation...');
},
{ deep: true, sync: true }
);
if (oldVm) {
Vue.nextTick(() => oldVm.$destroy()); // 卸载之前重新搞一个新的实例
}
}
3.5.实现命名空间
1
2
3
4
5
6
7
8
9
// vuex/module/module-collection.js
getNamespace(path) {
let module = this.root;
return path.reduce((namespace, key) => {
module = module.getChild(key);
console.log(module);
return namespace + (module.namespaced ? key + '/' : '');
}, '');
}
1
2
3
4
// vuex/module/module.js
get namespaced() {
return !!this._raw.namespaced;
}
在绑定属性时增加命名空间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// vuex/store.js
function installModule(store, path, module, rootState) {
let namespaced = store.modules.getNamespace(path);
if (path.length > 0) {
// 是子模块 需要将数据处理到rootState上
let parent = path.slice(0, -1).reduce((memo, current) => {
return memo[current];
}, rootState);
// 添加一个不存在的属性需要使用Vue.set方法 否则无效
store.withCommitting(() => {
Vue.set(parent, path[path.length - 1], module.state);
});
}
// 将循环的方法放到类中来维护 Object.entries(module._raw.mutations)
module.forEachMutation((mutationKey, mutationValue) => {
store.mutations[namespaced + mutationKey] =
store.mutations[namespaced + mutationKey] || [];
store.mutations[namespaced + mutationKey].push((payload) => {
store.withCommitting(() => {
mutationValue(getState(store, path), payload);
});
store._subscribes.forEach((callback) =>
callback({ type: mutationKey }, store.state)
);
});
});
module.forEachAction((actionKey, actionValue) => {
store.actions[namespaced + actionKey] =
store.actions[namespaced + actionKey] || [];
store.actions[namespaced + actionKey].push((payload) => {
actionValue(store, payload);
});
});
module.forEachGetter((getterKey, getterValue) => {
// 应该先判断一下store.getters是否有这个属性 如果有就不必要再注册
if (store.wrapperGetters[namespaced + getterKey]) {
return console.error('duplicate key');
}
store.wrapperGetters[namespaced + getterKey] = () => {
return getterValue(getState(store, path));
};
});
// 将循环的方法放到类中来维护 Object.entries(module._children)
module.forEachChildren((childName, childValue) => {
installModule(store, path.concat(childName), childValue, rootState);
});
}
3.6.注册模块
1
2
3
4
5
6
7
8
9
10
11
// vuex/store.js
registerModule(path, rawModule) {
// 如果是一个字符串就包装成数组
if (!Array.isArray(path)) {
path = [path];
}
this.modules.register(path, rawModule);
// 安装模块的时候 需要传入的是包装后的不是用户写的
installModule(this, path, rawModule.wrapperModule, this.state); // 将自己生产的模块装到全局上
resetVM(this, this.state);
}
实现模块的注册,就是将当前模块注册到_modules 中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function resetVM(store, state) {
+ let oldVm = store.vm; // 拿到以前的
const computed = {};
store.getters = {};
forEachValue(store.wrapperGetters, (getterKey, getterValue) => {
computed[getterKey] = () => getterValue(store.state);
Object.defineProperty(store.getters, getterKey, {
get: () => store.vm[getterKey], // vuex中的getters只有get
});
});
store.vm = new Vue({
data: {
$$state: state,
},
computed,
});
store.vm.$watch(
() => store.vm._data.$$state,
() => {
console.assert(store._commiting, 'outside mutation...');
},
{ deep: true, sync: true }
);
if (oldVm) {
+ Vue.nextTick(() => oldVm.$destroy()); // 卸载之前重新搞一个新的实例
}
}
销毁上次创建的实例
四.插件机制
4.1.使用方式
1
2
3
4
5
6
7
8
9
10
11
12
// store/modules/index.js
const persitsPlugin = (store) => {
let persists = localStorage.getItem('VUEX_PERSITS');
if (persists) {
store.replaceState(JSON.parse(persists));
}
store.subscribe((mutation, state) => {
localStorage.setItem('VUEX_PERSITS', JSON.stringify(state));
});
};
plugins: [persitsPlugin],
这里要实现 subscribe、replaceState 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// vuex/store.js
// 执行插件
if (options.plugins) {
options.plugins.forEach((plugin) => plugin(this));
}
subscribe(callback) {
this._subscribes.push(callback);
}
replaceState(newState) {
// 如果直接将状态替换会导致代码里用的是以前的 状态显示的是最新 改以前的不会导致视图更新
this.withCommitting(() => {
this.vm._data.$$state = newState;
});
}
4.2.获取最新状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function getState(store, path) {
return path.reduce((newState, current) => {
return newState[current];
}, store.state);
}
// 将循环的方法放到类中来维护 Object.entries(module._raw.mutations)
module.forEachMutation((mutationKey, mutationValue) => {
store.mutations[namespaced + mutationKey] =
store.mutations[namespaced + mutationKey] || [];
store.mutations[namespaced + mutationKey].push((payload) => {
store.withCommitting(() => {
mutationValue(getState(store, path), payload);
});
store._subscribes.forEach((callback) =>
callback({ type: mutationKey }, store.state)
);
});
});
调用 mutation 时传入最新状态
五.区分 mutation 和 action
1
2
3
4
5
withCommitting(fn) {
this._commiting = true;
fn(); // 如果这个函数是异步执行 那么_commiting会先变为false
this._commiting = false;
}
1
2
3
4
5
6
7
store.vm.$watch(
() => store.vm._data.$$state,
() => {
console.assert(store._commiting, 'outside mutation...');
},
{ deep: true, sync: true }
);
增加同步 watcher,监听状态变化
1
2
3
store.withCommitting(() => {
mutationValue(getState(store, path), payload);
});
只有通过 mutation 更改状态,断言才能通过
1
2
3
4
5
6
replaceState(newState) {
// 如果直接将状态替换会导致代码里用的是以前的 状态显示的是最新 改以前的不会导致视图更新
this.withCommitting(() => {
this.vm._data.$$state = newState;
});
}
1
2
3
4
// 添加一个不存在的属性需要使用Vue.set方法 否则无效
store.withCommitting(() => {
Vue.set(parent, path[path.length - 1], module.state);
});
内部更改状态属于正常更新,所以也许用 withCommitting 进行包裹