掘金上有一篇用 150 行代码实现 Vuex 80%的功能这篇文章,我非常好奇。
拜读之后,萌发了一个重新造一个“轮子”的想法,最后没想到,实现起来非常简单。
那么就写了这么一篇文章,来分析分析
Store是一个仓库,而且是一个容器,并且包含应用中的绝大部分state,就是状态。
考虑下面的代码:
State
import Vuex from 'vuex';
const store = new Vuex.Store({
state: {
counter: 1
}
});
Store是一个class,负责这个整个状态的变更。
那么我们可以将实例化的Store直接实例化后,可以使用state。
class Store {
constructor(options) {
this.options = options;
}
get state() {
return this.options.state;
}
}
const store = new Store({
state: {
counter: 1
}
});
console.log(store.state); // counter
不过在此之前,我们需要将他初始化到Vue上面去。
/**
* @description vuex初始化
* */
function vuexInit() {
const options = this.$options;
if (options.store) {
this.$store =
typeof options.store === 'function' ? options.store() : options.store;
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store;
}
}
接着在Store这个类中,添加这样的代码:
class Store {
+ constructor(options,Vue) {
+ Vue.mixin({beforeCreate:vuexInit});
this.options = options;
+ }
get state() {
return this.options.state;
}
}
const store = new Store({
state: {
counter: 1
}
});
这样就可以直接使用 Vue 的$store 了:
const __vm__ = new Vue({
render: h => h(App)
});
console.log(__vm__.$store.counter);
接下来,开始用mutaions来改变状态,首先需要做的是注册mutations。
注册mutaions做了两件事:
- 拦截
mutations的函数 - 将
mutations里的函数传入作用域。
import { forEach } from 'lodash';
function registerMutations(store, mutationName, mutationFn) {
store.mutations[mutationNmae] = () => {
mutationFn.call(store, store.state);
};
}
然后,遍历整个mutations。
forEach(store.mutations, (mutationFn, mutationName) => {
// 这里的this是指整个Store
registerMutations(this, mutationName, mutationsFn);
});
这样就可以修改state了,但是还有一个问题是,它无法变成响应式的。
那么我们可以直接使用 Vue,将其变成响应式的。
class Store {
constructor(options, Vue) {
this.__vm__ = new Vue({
data: {
state: options.state
}
});
}
get state() {
return this.__vm__.data.state;
}
}
有了mutations,那么我们就需要提交mutation,而且commit及其好写:
class Store {
constructor(options = {}, Vue) {
const { commit } = this;
// 让commit绑定到this上面
this.commit = type => commit.call(this, type);
this.mutations = {};
}
commit(type) {
this.mutations[type]();
}
}
那么完整的Store代码就是:
class Store {
constructor(options = {}, Vue) {
Vue.mixin({ beforeCreate: vuexInit });
this.options = options;
this.mutations = {};
const { dispatch, commit } = this;
this.commit = type => {
return commit.call(this, type);
};
forEach(options.mutations, (mutationFn, mutationName) => {
registerMutation(this, mutationName, mutationFn);
});
this.__vm__ = new Vue({
data: {
state: options.state
}
});
}
get state() {
return this.__vm__.data.state;
}
commit(type) {
this.mutations[type]();
}
}
function vuexInit() {
const options = this.$options;
if (options.store) {
this.$store =
typeof options.store === 'function' ? options.store() : options.store;
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store;
}
}
Actions
Vuex 的官方定义是:
- Action 提交的是 mutation,而不是直接变更状态
- Action 可以包含任何的异步操作
所以 action 里面的操作是 commit,而非变更状态:
它的用法是:
new Vuex.Store({
state: {
counter: 1
},
mutations: {
addCounter(state, payload) {
state.counter += payload;
}
},
actions: {
async addCounter({ commit }) {
const value = await Axios('localhost:8080');
commit('addCounter', value);
}
}
});
它和Mutation的注册相似,且call入的值类似于:
function registerAction(store, actionName, actionFn) {
store.actions[actionName] = () => {
actionFn.call(store, store);
};
}
接着,遍历所有 actions,并注册:
forEach(store, (actionsFn, acrionsName) => {
registerAction(this, actionsName, actionsFn);
});
Actions 的分发和mutation类似:
import { forEach } from 'lodash';
class Store {
constructor(options = {}) {
this.options = options;
const { dispatch } = this;
this.actions = [];
this.dispatch = type => dispatch.call(type);
forEach(store, (actionsFn, acrionsName) => {
registerAction(this, actionsName, actionsFn);
});
}
dispatch(type) {
return this.actions[type]();
}
}
function registerAction(store, actionName, actionFn) {
store.actions[actionName] = () => {
actionFn.call(store, store);
};
}
然后结合mutations的commit,完成一次异步更新,代码如下:
import { forEach } from 'lodash';
export default class Store {
constructor(options = {}, Vue) {
Vue.mixin({ beforeCreate: vuexInit });
this.options = options;
this.getters = {};
this.mutations = {};
this.actions = {};
const { dispatch, commit } = this;
this.commit = type => {
return commit.call(this, type);
};
this.dispatch = type => {
return dispatch.call(this, type);
};
forEach(options.actions, (actionFn, actionName) => {
registerAction(this, actionName, actionFn);
});
forEach(options.mutations, (mutationFn, mutationName) => {
registerMutation(this, mutationName, mutationFn);
});
this.__vm__ = new Vue({
data: {
state: options.state
}
});
}
get state() {
return this.options.state;
}
commit(type) {
this.mutations[type]();
}
dispatch(type) {
return this.actions[type]();
}
}
function registerMutation(store, mutationName, mutationFn) {
store.mutations[mutationName] = () => {
mutationFn.call(store, store.state);
};
}
function registerAction(store, actionName, actionFn) {
store.actions[actionName] = () => {
actionFn.call(store, store);
};
}
function vuexInit() {
const options = this.$options;
if (options.store) {
this.$store =
typeof options.store === 'function' ? options.store() : options.store;
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store;
}
}
这样,就可以直接使用异步更改状态了。
import Vue from 'vue';
const store = new Vuex.Store(
{
state: {
counter: 1
},
mutations: {
updateCounter(state) {
state.counter++;
}
},
actions: {
updateCounterAsync({ commit }) {
setTimeout(function() {
commit('updateCounter');
}, 1000);
}
}
},
Vue
);
const __store__ = new Vue({
store
}).$mount('.app');
Getter
Geetter 可以看做为Vuex中的computed,带有一种依赖的关系的方法。
首先,写一个注册函数。因为它和Mutations和Actions一样,是一个函数。
function registerGetter(store, getterName, getterFn) {
Object.defineProperty(store.getters, getterName, {
get() {
return getterFn(store.state);
}
});
}
接着遍历所有的getter,让它变成重新构造一遍方法即可。
import { forEach } from 'lodash';
class Store {
constructor(options = {}) {
Vue.mixin({ beforeCreate: vuexInit });
this.options = options;
this.getters = {};
forEach(options.getters, (getterFn, getterName) => {
registerGetter(this, getterName, getterFn);
});
}
}
function registerGetter(store, getterName, getterFn) {
Object.defineProperty(store.getters, getterName, {
get() {
return getterFn(store.state);
}
});
}
module
这是最复杂的一部分,相当于将其重写了一遍。
首先,我们先定义一个 ModuleCollection 类,然后将模块定义的方法:
class ModuleCollection {
constructor(rawRootModule) {
this.register([], rawRootModule);
}
register(path, _rawModule) {
const newModule = {
_children: {}, // 嵌入或者递归到_children上面去,
_rawModule, // 展开递归
state: _rawModule.state
};
if (path.length === 0) {
// 如果模块的key数组长度为0的话,直接赋值给root
this.root = newModule;
} else {
// 如果不是,那么就直接找到module的值,然后降级到最后一个值
const parent = path.slice(0, -1).reduce((module, key) => {
return module._children(key);
}, this.root);
// 最后一个path为新模块
parent._children[path[path.length - 1]] = newModule;
}
// 直接遍历所有module,然后注册为模块
if (_rawModule.module) {
forEach(_rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule);
});
}
}
}
既然找出了所有可能存在module的状态,那么剩下就是将所有的module全部变成可以操作store
那么 installceModule 函数则是:
function installModule(store, rootState, path, module) {
if (path.length > 0) {
const getLastModule = path[path.length - 1];
// 这是rootState,然后重新设置为响应式的方式
_Vue.set(rootState, getLastModule, module.state);
}
const context = {
dispatch: store.dispatch,
commit: store.commit
};
const local = Object.defineProperties(context, {
getters: {
get: () => store.getters
},
state: {
get: () => {
let state = store.state;
return path.length
? path.reduce((state, key) => state[key], state)
: state;
}
}
});
if (module._rawModule.actions) {
forEach(module._rawModule.actions, (actionFn, actionName) => {
registerAction(store, actionName, actionFn, local);
});
}
if (module._rawModule.getters) {
forEach(module._rawModule.getters, (getterFn, getterName) => {
registerGetter(store, getterName, getterFn, local);
});
}
if (module._rawModule.mutations) {
forEach(module._rawModule.mutations, (mutationFn, mutationName) => {
registerMutation(store, mutationName, mutationFn, local);
});
}
// 递归注册所有的modules
forEach(module._children, (child, key) => {
installModule(store, rootState, path.concat(key), child);
});
}
最后Vuex的源码为:
let _Vue;
export class Store {
constructor(options = {}, Vue) {
_Vue = Vue;
Vue.mixin({ beforeCreate: vuexInit });
this.getters = {};
this._mutations = {}; // 在私有属性前加_
this._wrappedGetters = {};
this._actions = {};
this._modules = new ModuleCollection(options);
const { dispatch, commit } = this;
this.commit = type => {
return commit.call(this, type);
};
this.dispatch = type => {
return dispatch.call(this, type);
};
const state = options.state;
const path = []; // 初始路径给根路径为空
installModule(this, state, path, this._modules.root);
this._vm = new Vue({
data: {
state: state
}
});
}
get state() {
// return this.options.state; // 无法完成页面中的双向绑定,所以改用this._vm的形式
return this._vm._data.state;
}
commit(type) {
this._mutations[type].forEach(handler => handler());
}
dispatch(type) {
return this._actions[type][0]();
}
}
class ModuleCollection {
constructor(rawRootModule) {
this.register([], rawRootModule);
}
register(path, rawModule) {
const newModule = {
_children: {},
_rawModule: rawModule,
state: rawModule.state
};
if (path.length === 0) {
this.root = newModule;
} else {
const parent = path.slice(0, -1).reduce((module, key) => {
return module._children(key);
}, this.root);
parent._children[path[path.length - 1]] = newModule;
}
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule);
});
}
}
}
function installModule(store, rootState, path, module) {
if (path.length > 0) {
const parentState = rootState;
const moduleName = path[path.length - 1];
_Vue.set(parentState, moduleName, module.state);
}
const context = {
dispatch: store.dispatch,
commit: store.commit
};
const local = Object.defineProperties(context, {
getters: {
get: () => store.getters
},
state: {
get: () => {
let state = store.state;
return path.length
? path.reduce((state, key) => state[key], state)
: state;
}
}
});
if (module._rawModule.actions) {
forEachValue(module._rawModule.actions, (actionFn, actionName) => {
registerAction(store, actionName, actionFn, local);
});
}
if (module._rawModule.getters) {
forEachValue(module._rawModule.getters, (getterFn, getterName) => {
registerGetter(store, getterName, getterFn, local);
});
}
if (module._rawModule.mutations) {
forEachValue(module._rawModule.mutations, (mutationFn, mutationName) => {
registerMutation(store, mutationName, mutationFn, local);
});
}
forEachValue(module._children, (child, key) => {
installModule(store, rootState, path.concat(key), child);
});
}
function registerMutation(store, mutationName, mutationFn, local) {
const entry =
store._mutations[mutationName] || (store._mutations[mutationName] = []);
entry.push(() => {
mutationFn.call(store, local.state);
});
}
function registerAction(store, actionName, actionFn, local) {
const entry = store._actions[actionName] || (store._actions[actionName] = []);
entry.push(() => {
return actionFn.call(store, {
commit: local.commit,
state: local.state
});
});
}
function registerGetter(store, getterName, getterFn, local) {
Object.defineProperty(store.getters, getterName, {
get: () => {
return getterFn(local.state, local.getters, store.state);
}
});
}
// 将对象中的每一个值放入到传入的函数中作为参数执行
function forEachValue(obj, fn) {
Object.keys(obj).forEach(key => fn(obj[key], key));
}
function vuexInit() {
const options = this.$options;
if (options.store) {
// 组件内部设定了store,则优先使用组件内部的store
this.$store =
typeof options.store === 'function' ? options.store() : options.store;
} else if (options.parent && options.parent.$store) {
// 组件内部没有设定store,则从根App.vue下继承$store方法
this.$store = options.parent.$store;
}
}
缺点
- 它无法实现传入载荷进函数内
- 没有
mapStates,mapMutations,mapActions - 没有
namespace