想要更好的使用一个插件,可以尝试理解其实现的方式。
当然,了解一个优秀的插件,本身也会增强自己的能力。
本文,努力从零开始实现一个简易版的vuex,期间会用到很多编程思想,希望自己越来越灵活使用。
TL;DR
state是响应式的,巧用Vuegetters用Object.defineProperty实现和state的紧密相关mutations是定义commit方法,commit的this需要固定为store实例actions是定义dispatch,逻辑神似mutations- 翻到文末可以直接看
vuex.js的简易版代码
vuex 的初版样子
先可以用vue create xx创建一个项目,不带vuex的。
先看看,如果有vuex插件的main.js。
!!!特别注意
{state:{},mutations:...},是用户传的参数- store 虽然可以
this.$store.state,但这个 state 不完全是用户传的 state,而是处理过的 state,这两有本质区别 - 同样,用户传过来的其他属性,也会做处理,这样才有后期的
this.$store.getters.xx等等 - 换言之,
store就是对用户传的参数做各种处理,以方便用户操作她的数据。
从这推理出vuex,应该具有的特征:
Vue.use表明,vuex 肯定有install方法new Vuex.Store表明,vuex 导出对象里,有个Store的类- 每个组件内部都可以
this.$store表明,需要注入$store
如果对插件一脸懵的话,可以简单看下vue 插件的入门说明
第一版vuex.js就出来了:
但这样,$store和store实例并没有挂钩,此时可以借助Vue.mixins的beforeCreate钩子拿到当前的 Vue 实例,从而拿到实例的$options 。
export default {
install(Vue) {
Vue.mixin({
beforeCreate() {
// 这里的this是vue的实例,其参数store就是store实例
(!Vue.prototype.$store) && (Vue.prototype.$store = this.$options.store;)
}
});
},
Store
};
改进:不要轻易在原型上面添加属性,应该只在根实例有store的时候才设置$store,子实例会拿到根实例的$store
github源码 切换到c1分支
处理用户传的 state
store 实例的state可以出现在视图里,值变化的时候,视图也一并更新。
所以,state是被劫持的,这里投机取巧的用下Vue。
// vuex.js
class Store {
constructor(options) {
this.options = options;
this.state = new Vue({ data: options.state });
}
}
<!-- App.vue -->
<div id="app">
{{ $store.state.a }}
<button @click="$store.state.a++">
增加
</button>
</div>
github源码 切换到c2分支
!!!因为state是用Vue进行响应式,所有vuex重度依赖vue,不能脱离vue使用
处理用户传的 getters
- 用户传的
getters是一个函数集合 - 但是实际使用中,属性值是函数的返回值
- 属性依旧是劫持的,这边因为是函数,所以不能再投机取巧了
// vuex.js
constructor(options) {
this.options = options;
this.state = new Vue({ data: options.state });
if (options.getters) {
this.getters = {};
Object.keys(options.getters).forEach(key => {
// 这里必须是属性劫持
Object.defineProperty(this.getters, key, {
get: () => {
return options.getters[key](this.state);
}
});
});
}
}
// main.js
state: { a: 1, b: 2 },
getters: { a1(state) { return state.a + 1; } }
<!-- app.vue -->
<div id="app">
{{ $store.state.a }} {{ $store.getters.a1 }}
<button @click="$store.state.a++">
增加
</button>
</div>
github源码 切换到c3分支
处理 mutations
mutations,传的参数是一个函数集合的对象,使用的时候commit('函数名',payload)
代码翻译:
mutations:{
addA(state,payload){state.a+=payload}
}
// 使用的时候
this.$store.commit('addA',2)
由此推理出,vuex 其实写了一个commit方法。这个就很简单了,直接溜上来。
// vuex.js
class Store {
constructor(options) {
// ...
if (options.mutations) {
this.mutations = { ...options.mutations };
}
}
commit(mutationName, ...payload) {
console.log(mutationName, ...payload);
this.mutations[mutationName](this.state, ...payload);
}
}
// <button @click="$store.commit('addA', 2)"> 增加 </button>
const store = new Vuex.Store({
state: { a: 1, b: 2 },
getters: {
a1(state) {
return state.a + 1;
},
},
mutations: {
addA(state, num) {
state.a += num;
},
},
});
github源码 切换到c4分支
处理 actions
actions和mutations是很相似的。
actions:{
// 注意!!!,这里的第一个参数是store实例
addA({commit},payload){setTimeout(()=>{commit('addA',payload)},1000)}
}
// 使用的时候
this.$store.dispatch('addA',100)
这下更容易了,直接copy
commit(mutationName, ...payload) {
this.mutations[mutationName](this.state, ...payload);
}
dispatch(actionName, ...payload) {
// 注意这里是this,不是this.state
this.actions[actionName](this, ...payload);
}
// <button @click="$store.dispatch('addA', 2)"> 1s后增加100 </button>
const store = new Vuex.Store({
// ...
actions: {
addA(store, num) {
setTimeout(() => {
store.commit("addA", num);
}, 1000);
}
},
});
github源码 切换到c5分支
优化
- commit做处理的时候,最好用下切片思维,这样方便修改逻辑
- commit里面的
this,最好固定执行store实例,因为这样在action那边的时候,可以直接解构赋值 - action也一样
// vuex.js
constructor(options){
// ...
if (options.mutations) {
this.mutations = {};
Object.keys(options.mutations).forEach(mutationName => {
// 切片思维,这里上下都可以加逻辑
this.mutations[mutationName] = (...payload) => {
options.mutations[mutationName](...payload);
};
});
}
}
// 将this始终执行store实例
commit = (mutationName, ...payload) => {
this.mutations[mutationName](this.state, ...payload);
};
action操作一样,不在赘述代码。
actions: {
addA({commit}, num) {
// 这里可以解构了!!!
setTimeout(() => {
commit("addA", num);
}, 1000);
}
},
github源码 切换到c6分支。
还有模块空间的内容,考虑到篇幅较长,就不在本文继续了。
附注:vuex.js的所有代码
let Vue;
class Store {
constructor(options) {
this.options = options;
this.state = new Vue({ data: options.state });
if (options.getters) {
this.getters = {};
Object.keys(options.getters).forEach(key => {
Object.defineProperty(this.getters, key, {
get: () => {
return options.getters[key](this.state);
}
});
});
}
if (options.mutations) {
this.mutations = {};
Object.keys(options.mutations).forEach(mutationName => {
this.mutations[mutationName] = (...payload) => {
options.mutations[mutationName](...payload);
};
});
}
if (options.actions) {
this.actions = {};
Object.keys(options.actions).forEach(actionName => {
this.actions[actionName] = (...payload) => {
options.actions[actionName](...payload);
};
});
}
}
commit = (mutationName, ...payload) => {
this.mutations[mutationName](this.state, ...payload);
};
dispatch = (actionName, ...payload) => {
this.actions[actionName](this, ...payload);
};
}
export default {
install(_Vue) {
Vue = _Vue;
Vue.mixin({
beforeCreate() {
// 这里的this是vue的实例,其参数store就是store实例
const hasStore = this.$options.store;
// 根实例的store
hasStore
? (this.$store = this.$options.store)
: this.$parent && (this.$store = this.$parent.$store);
}
});
},
Store
};