如何实现一个简单的Vuex
在Vue项目中我们经常会用到Vuex来统一管理我们的组件状态,在使用Vuex的过程中我经常会思考以下几个问题:
- Vue.use(Vuex)发生了什么?
- 为什么store.state中的数据发生了改变会导致组件重新渲染?
- Vuex中的getters是如何实现的?
- Vuex中mutatuion是如何实现的?
- Vuex的action是如何实现的?
在这边文章中我会通过实现一个简易版Vuex的方式来对以上的问题作出一一回答。
1. Vue.use(Vuex)发生了什么?
在Vue项目中,当我们要使用Vuex时有三个步骤是我们一定要做的。第一是使用Vue.use(Vuex)将Vuex以Vue插件的形式引入到我们的项目中,第二是创建一个Store实例,第三是将Store实例添加到Vue根组件实例的选项对象中。如下代码所示:
import Vue from 'vue'
import Vuex from 'vuex'
import App from './App.vue'
// 步骤一,以Vue插件的形式将Vuex引入到我们的项目中
Vue.use(Vuex)
// 步骤二,创建一个Store实例
cosnt store = new Vuex.Store({
state: {
counter: 0,
},
mutations: {
add(state) {
state.counter++
}
},
actions: {
add({ commit }) {
setTimeout(() => {
commit('add')
}, 1000);
}
},
})
// 步骤三,将Store实例添加到Vue根组件实例的选项对象中
new Vue({
store,
render: h => h(App)
})
以上代码是我们使用Vuex时在main.js中的常规写法。那么当我们使用Vue.use(Vuex)时发生了什么呢?**答案就是通过执行Vue.use(Vuex)我们将store对象挂载到了每个组件实例的store的形式访问到store对象。
好了,现在我们知道了当我们使用Vue.use(Vuex)时发生了什么,那么Vuex是如何通过Vue.use(Vuex)实现将store对象挂载到每个组件实例的$store属性上呢?答案就是通过Vue.mixin(全局混入)。我们可以通过全局混入beforeCreate钩子函数并在钩子函数中实现类似于如下的代码:
this.$store = store // 这里的this指的是当前组件实例
这样每个组件实例在创建时都会调用该钩子函数,从而实现将store对象挂载到$store属性中。代码实现如下:
let Vue
class Store {}
function install(_Vue) {
Vue = _Vue
Vue.mixin({
beforeCreate() {
let options = this.$options
if (options.store) {
this.$store = options.store
} else if (options.parent && options.parent.store) {
this.$store = options.parent.store
}
}
})
}
export default {
Store,
install,
}
首先,当我们使用Vue.use方法注册插件时,Vue会调用插件的install方法。因此我们必须实现install方法,并在install方法中实现我们的混入逻辑。当调用Vue.use方法时Vue会将Vue的构造函数作为参数传入插件的install方法中,这样我们可以很方便使用全局混入方法。其次,在我们实现的beforeCreate钩子函数中主要逻辑就在if 和 else if的代码块中。当当前组件实例为根组件实例时进入if代码块中,当组件实例不是根组件实例时,就从该组件实例的父组件实例中取出store对象并赋值给this.$store。
2. 为什么store.state中的数据发生了改变会导致组件重新渲染?
我们知道当我们改变Vue选项对象中data属性中的数据时会导致组件的重新渲染,这是因为在创建实例的过程中选项对象的data属性中的数据被转变成了响应式数据。那么如果我们把store.state中的数据也变为响应式数据会怎样呢?如果这样做的话,那么当我们读取store.state中的数据时我们就会收集到依赖(渲染函数),变更store.state中的数据时就会触发渲染函数,导致组件重新渲染。那么问题二的答案便呼之欲出了,之所以store.state中的数据发生了改变会导致组件重新渲染,是因为store.state中的数据是响应式数据。
那么问题来了如果我们要实现一个Vuex,我们该如何让store.state中的数据变为响应式数据呢?这个答案很简单,我们只需创建一个Vue的实例并把store.state挂载到Vue选项对象的data属性中,这样当创建实例时store.state就会被转变为响应式数据。我们的代码如下:
class Store {
constructor(options) {
this._vm = new Vue({
data: {
?state: options.state
}
})
}
get state() {
return this._vm.$data.?state
}
}
function install(_Vue) {
Vue = _Vue
Vue.mixin({
beforeCreate() {
let options = this.$options
if (options.store) {
this.$store = options.store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
})
}
export default {Store, install}
我们在Store的构造函数中创建了一个Vue实例,并把options.state挂载到了Vue选项对象的data属性上,从而使得options.state转变成为响应式数据。为了避免对store.state进行更改,我们将store.state定义成了取值函数,这样就只能对store.state上属性进行修改而不能直接修改store.state。其实在Vuex中,state属性是定义在Store.prototype上的属性,并且重写了state属性的get和set方法,如下代码所示:
var prototypeAccessors$1 = { state: { configurable: true } };
prototypeAccessors$1.state.get = function () {
return this._vm._data.?state
};
prototypeAccessors$1.state.set = function (v) {
if ((process.env.NODE_ENV !== 'production')) {
assert(false, "use store.replaceState() to explicit replace store state.");
}
};
Object.defineProperties( Store.prototype, prototypeAccessors$1 );
其实我们所使用的取值函数其实就是Vuex中定义state属性所使用方式的语法糖而已。
3. Vuex中的getters是如何实现的?
在Vuex中还提供了getter属性,在Vuex的官网中我们可以看到对getter属性的解释。它相当于vue中的计算属性,只有当它所依赖的state中的数据发生变化时才会被重新求值。在问题2中我们通过将store.state挂载到Vue选项对象的data属性上实现了Vuex的state,那么对于与Vue中计算属性很像的getters我们自然会联想到通过Vue的计算属性来实现Vuex中的getters。代码如下:
let Vue
class Store {
constructor(options) {
this._vm = new Vue({
data: {
?state: options.state
},
computed: this.initGetters(options.getters)
})
}
get state() {
return this._vm.$data.?state
}
get getters() {
return this._vm
}
}
Store.prototype.initGetters = function (getters) {
let computed = {}
Object.keys(getters).forEach(key => {
computed[key] = () => getters[key](this.state)
})
return computed
}
function install(_Vue) {
Vue = _Vue
Vue.mixin({
beforeCreate() {
let options = this.$options
if (options.store) {
this.$store = options.store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
})
}
export default { Store, install};
getters的实现方式很简单,我们将从Store构造函数中传入的getters属性中的函数进行了一次包装,然后将包装后的getters对象赋值给Vue选项对象的compted属性,这样我们便以Vue计算属性的形式实现了Vuex。此外为了实现以this.$store.getters.key的形式访问到getters中的数据,我们在Store类中也定义了一个名为getters存取器。
在Vuex的源码中对于getters的实现和我们的思路是一样,就是将getters转换为Vue中的计算属性,从而实现依赖改变getter重新求值。
通过对问题2和问题3的解答,我们不难发现Vuex中对state和getters的实现都必须依赖于Vue,也就是为什么我们常说Vuex与Vue是相耦合的。
4. Vuex中mutatuion是如何实现的?
Vuex中mutatuion的实现原理还是比较简单的,就是当调用commit函数时,根据commit函数的第一个参数来判断是调用mutatuions中的哪个函数然后传参入参数并调用即可。
let Vue
class Store {
constructor(options) {
this._vm = new Vue({
data: {
?state: options.state
},
computed: this.initGetters(options.getters)
})
this._mutations = options.mutations
}
get state() {
return this._vm.$data.?state
}
get getters() {
return this._vm
}
commit = (type, payload) => {
this._mutations[type](this.state, payload)
}
}
Store.prototype.initGetters = function (getters) {
let computed = {}
Object.keys(getters).forEach(key => {
computed[key] = () => getters[key](this.state)
})
return computed
}
function install(_Vue) {
Vue = _Vue
Vue.mixin({
beforeCreate() {
let options = this.$options
if (options.store) {
this.$store = options.store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
})
}
export default { Store, install};
对于mutatuion的现实本质上就是如何实现一个commit函数。commit函数实现原理很简单,如上代码所示,就是根据type选择对应mutatuion函数,传入相关的参数并调用即可。
5. Vuex的action是如何实现的?
Vuex支持Action,Action与Mutation作用相同都是用来变更store.state中的数据。但是Action可以包含异步操作,当异步操作执行完成后再以Mutation的方式完成store.state中的数据变更。我们的代码实现如下:
let Vue
class Store {
constructor(options) {
this._vm = new Vue({
data: {
?state: options.state
},
computed: this.initGetters(options.getters)
})
this._mutations = options.mutations
this._actions = options.actions
}
get state() {
return this._vm.$data.?state
}
get getters() {
return this._vm
}
commit = (type, payload) => {
this._mutations[type](this.state, payload)
}
dispatch = (type, payload) => {
let context = {
commit: this.commit,
state: this.state,
}
this._actions[type](context, payload)
}
}
Store.prototype.initGetters = function (getters) {
let computed = {}
Object.keys(getters).forEach(key => {
computed[key] = () => getters[key](this.state)
})
return computed
}
function install(_Vue) {
Vue = _Vue
Vue.mixin({
beforeCreate() {
let options = this.$options
if (options.store) {
this.$store = options.store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
})
}
export default { Store, install};
Vuex中action的实现本质上就是对dispatch方法的实现,dispatch与commit原理差不多,最本质的区别是dispatch在调用对应的action函数时会将scommit方法作为参数传入到action函数中去,这样便可以让action方法做完异步操作后再调用commit方法实现对store.state中数据的修改。
后记
这篇文章通过对5个问题的解答实现了一个简易的Vuex插件。这个Vuex插件非常简单甚至可以说是捡漏,其中有些方法可能少传了一些参数。但这并不妨碍我们对Vuex原理的理解。当然在我们的实现中我并没有将Vuex的modules功能给实现出来,这个我准备留在以后实现,敬请期待!