从零开始 - 50行代码实现一个Vuex状态管理器
回顾下Vuex
先vue-cli工具直接创建一个项目,勾选Vuex,其他随意:
创建完毕自动安装依赖,之后启动项目,熟悉的helloworld ~ 简单写个demo运行看看,后面会逐步实现一个myVuex,来达到相同的期望运行结果:
src/store/index.js
:
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
age: 7
},
getters: { // 不用多说
getAge(state) { return state.age }
},
mutations: { // vuex约定了对state的操作函数都放在这里,使用commit方法触发
changeAge(state, data) {
state.age = data ? data : ++state.age
}
},
actions: { // vuex约定了异步类函数统一放在这里,dispatch方法触发
syncChangeAge({ state, commit }, data) {
state.age = 0
setTimeout(() => {
this.commit('changeAge', data) // 这里我还没弄懂待会怎么实现{commit}的读取,在真实的Vuex中这里不加this也是可以运行的
}, 1000);
}
},
modules: { /** vuex的模块化,先不实现modules功能,就不挖坑了 */ },
});
src/App.vue
:
<template>
<div id="app">
{{ showMe }}
<button @click="$store.commit("changeAge")">increase</button>
<button @click="$store.dispatch("syncChangeAge", 7)">reset</button>
</div>
</template>
<script lang="js">
import Vue from "vue";
export default Vue.extend({
name: "App",
computed: {
showMe() { return `我今年${this.$store.getters.getAge || "..."}岁了`; },
}
});
</script>
运行效果如下:
说明:点击增加按钮加一岁,点击重置按钮进入loading状态1秒后又设置为7岁,现在,把stroe中引入的import Vuex from "vuex";
改为自己的手动实现,达到跟这个demo一致的运行效果。
Ready Perfect
开始前还是先写出代码结构,创建 Vuex
文件夹,写入第一个index文件。
src/Vuex/index.js
:
class Store {
constructor(parameters) { // vuex的核心四件套
this.state = {}
this.getters = {}
this.mutations = {}
this.actions = {}
}
get state() {
return this.state
}
commit(fName, data) {
this.mutations[fName].forEach(mutation => mutation(data));
}
dispatch(fName, data) {
this.actions[fName].forEach(action => action(data));
}
}
export default { Store }
这样vuex的简单结构就写完了,接下来处理对实例传入的mutation和action的收集,然后提供commit和dispatch函数来执行。
创建 install
首先 store 中先是调用了 Vue.use(Vuex)
,让状态管理器注入到vue中,此时需要用到混入。
mixin 参考:vue全局混入
根据vue文档描述,使用use必须提供一个install函数,Vue会作为参数传入,参考:vueUse
src/Vuex/index.js
:
class Store {
.....
}
const install = (Vue) => {
Vue.mixin({
beforeCreate() {
const { store = null } = this.$options
if (store) {
this.$store = store
} else {
this.$store = this.$parent && this.$parent.$store
}
}
})
}
export default { Store, install }
绑定 state
在上一步创建install时引入了Vue,将其挂载到全局来创建一个实例对象,利用Vue中数据双向绑定来实现state:
src/Vuex/index.js
:
let _Vue
class Store {
constructor(parameters) {
const { state = { } } = parameters
this.$vue = new _Vue({ // new一个Vue实例接收用户传进的state
data: { state }
})
......
}
get state() { // 抛出Vue实例上挂载的 state
return this.$vue.state
}
......
}
const install = (Vue) => {
_Vue = Vue
......
}
.....
处理 getter
继续上面的代码
....
class Store {
constructor(parameters) {
.....
bindInstall(this, parameters)
}
.....
}
const install = (Vue) => { .... }
const bindInstall = (store, options) => {
// 处理getters
const { getters } = options
if (getters) {
Object.keys(getters).forEach(key => {
Object.defineProperty(store.getters, key, {
get() {
return getters[key](options.state)
}
})
})
}
}
export default { Store, install }
到这里,可以将 src/store/index
中的引入改成我们自己的了:
// import Vuex from "vuex";
import Vuex from "../Vuex";
.....
将例子运行,将看到已经成功拿到store中的getter,继续完善
处理 mutations 与 actions
继续完善刚才的bindInstall代码:
....
class Store { ..... }
const install = (Vue) => { .... }
const bindInstall = (store, options) => { // 两边收集都比较相似
const { getters, mutations, actions } = options
if (getters) { ... }
if (mutations) {
Object.keys(mutations).forEach(mutationName => {
let storeMutations = store.mutations[mutationName] || []
storeMutations.push(data => {
mutations[mutationName].call(store, store.state, data) // mutations中的函数第一个参数是state,第二个是值
})
store.mutations[mutationName] = storeMutations
})
}
if (actions) {
Object.keys(actions).forEach(actionName => {
let storeActions = store.actions[actionName] || []
storeActions.push(data => {
actions[actionName].call(store, store, data) // 这里我第一个参数先直接返回了实例对象,还不知道如何实现vuex中的效果
})
store.actions[actionName] = storeActions
})
}
}
export default { Store, install }
保存,运行测试 - 和最初的demo结果一致,至此实现了核心的vuex状态管理器
以下是 Vuex/index.js
完整代码
let _Vue
class Store {
constructor(parameters) {
const { state = {} } = parameters
this.$vue = new _Vue({ data: { state } })
this.getters = {}
this.mutations = {}
this.actions = {}
bindInstall(this, parameters)
}
get state() { return this.$vue.state }
commit(fName, data) { this.mutations[fName].forEach(mutation => mutation(data)) }
dispatch(fName, data) { this.actions[fName].forEach(action => action(data)) }
}
const install = (Vue) => {
_Vue = Vue
Vue.mixin({
beforeCreate() {
const { store = null } = this.$options
this.$store = store ? store : this.$parent ? this.$parent.$store : null
}
})
}
const bindInstall = (store, options) => {
const { getters, mutations, actions } = options
if (getters) {
Object.keys(getters).forEach(key => {
Object.defineProperty(store.getters, key, {
get() { return getters[key](options.state) }
})
})
}
if (mutations) {
Object.keys(mutations).forEach(mutationName => {
let storeMutations = store.mutations[mutationName] || []
storeMutations.push(data => { mutations[mutationName].call(store, store.state, data) })
store.mutations[mutationName] = storeMutations
})
}
if (actions) {
Object.keys(actions).forEach(actionName => {
let storeActions = store.actions[actionName] || []
storeActions.push(data => { actions[actionName].call(store, store, data) })
store.actions[actionName] = storeActions
})
}
}
export default { Store, install }