一、回顾一下官方vuex插件的使用
要想自己实现一个vuex插件,就必须先了解一下vuex插件的基本使用,我们在使用vuex的时候,通常会定义一个store.js文件,里面主要就是干了以下几件事:
① 引入vuex模块;
import Vuex from 'vuex';
② 引入Vue并使用vuex,即所谓的安装vuex插件
import Vue from 'vue';
Vue.use(Vuex);
③ 创建Store对象并对外暴露
④ 配置Store对象,即在创建Store对象的时候传递一个options参数,里面主要包括state(vuex的状态数据)、getters(vuex中派生的状态数据)、mutations(同步改变状态数据的方法)、actions(异步改变状态数据的方法)
export default new Vuex.Store({
state: {
count: 0
},
getters: {
newCount(state) {
return state.count * 10;
}
},
mutations: {
syncAdd(state, payload) { // 同步
state.count += payload; // 将状态数据中的count值加payload
},
syncMinus(state, payload) { // 同步
state.count -= payload; // 将状态数据中的count值减payload
}
},
actions: {
asyncMinus({commit}, payload) { // 异步
setTimeout(() => {
commit("syncMinus", 10);
}, 1000);
});
}
});
当然,还有最后一步就是在main.js中引入store.js对外暴露的store对象,并且将该store对象配置到Vue项目的根实例上,至此,就可以在项目中使用vuex了,所谓使用vuex,就是在页面中通过this.$store获取和修改store对象中的状态数据。
二、Vuex原理解析
Vuex本质是用一个全局的Vue对象实例作为数据源,而这个全局的Vue实例对象Store又会被挂载到整个App Vue实例上。
const store = new Vue({
data() {
return {
msg: "vuex"
}
}
});
const app = new Vue({
el: "#app",
beforeCreate () {
// 将全局的Vuex实例对象store挂载到app实例上
this.$store = store;
},
mounted () {
setTimeout(() => {
// 2秒后修改Vuex中的数据
store.msg = "vuex changed";
}, 2000);
}
});
<div id="app">
msg - {{this.$store.msg}}
</div>
我们的模板中使用到了全局的Vuex实例store中的msg数据,当App实例渲染的时候,会创建一个渲染Watcher,渲染Watcher在创建虚拟DOM的时候就会访问到store中的数据,进而触发store中msg的get方法收集依赖,同时会将当前渲染Watcher添加到msg对应的Dep对象中的观察者列表中,当store中的数据msg发生改变后,就会触发对应Dep对象的notify方法通知其观察者列表中的所有观察者,由于渲染Watcher已经添加到了store对象中msg对应的Dep的观察者列表中,所以渲染Watcher就会触发更新,渲染Watcher更新就会导致页面更新。
三、开始实现自己的vuex插件
① 因为我们需要通过Vuex.Store来创建store对象,所以,vuex插件导出的是一个对象,并且其中有一个Store类,在vuex插件中声明一个Store类,同时在创建Store对象的时候需要传递一个options参数配置对象,有state、getters、mutations、actions等属性配置,如:
class Store {
constructor(options) {
}
}
export default { // vuex插件导出的是一个对象
Store
}
② 由于vuex插件需要安装才能使用,所以vuex插件导出的对象中必须包含一个install方法,添加一个install方法,install方法会接收Vue构造函数,为了方便使用,可以在插件中将Vue的构造函数进行保存,vuex本质是一个全局的大对象,数据可以被所有组件共享,所以我们需要将这个store对象注入到所有组件内,以便任何一个组件都可以通过this.$store获取到注入到根组件内的store对象,如:
let Vue = null;
const install = (VueConstructor) => {
Vue = VueConstructor; // 将Vue构造函数保存起来
Vue.mixin({ // Vue组件是从外到内逐级渲染的过程
beforeCreate () { // 在组件创建之前,不会覆盖组件中的beforeCreate
if(this.$options && this.$options.store) { // 根组件实例对象,即main.js中new Vue({})创建的实例
this.$store = this.$options.store;
} else { // 非根组件实例对象
this.$store = this.$parent && this.$parent.$store;
}
}
});
}
export default { // vuex插件导出的是一个对象
Store,
install
}
③ 接下来就是要处理创建Store对象的时候传递进来的options配置对象了,首先我们先对vuex的state状态数据进行处理,我们通过options传递进来的state状态数据是一个普通对象,如果直接将这个state对象保存起来,那么当组件修改状态数据的时候,页面中就无法感知到Store中状态数据发生了变化,因为状态数据是一个普通对象,没有添加getter和setter,所以我们可以直接把状态数据传递给Vue的构造函数,这样状态数据就会被添加到这个Vue实例对象上了,也就是说Store对象的状态数据也是一个Vue实例对象,当页面中有使用到Store中的数据,那么该数据就会被当做Watcher添加到观察者列表中,当Store中数据发生变化,就会通知所有观察者进行更新,从而实现页面中数据更新。
class Store {
constructor(options) {
this._vm = new Vue({ // 将传递的state状态数据传递给Vue并创建Vue实例对象,以便页面中使用到state数据的时候能够更新页面
data: {
state: options.state
}
});
}
get state() { // 以便页面中可以通过this.$store.state获取到状态数据
return this._vm.state; // 从创建的Vue实例对象中获取state状态数据
}
}
④ 接下来就是要处理传递进来的getters,因为getters对象中,每个getter都是一个函数,而使用的时候,我们通过点getter的名称获取的到派生出来的新的状态,即执行getter对应函数,所以我们需要在获取getter的时候进行拦截,然后执行getter函数获取到返回值之后再返回作为新的状态值,所以我们需要通过defineProperty进行定义,如:
class Store {
constructor(options) {
const getters = options.getters || {}; // 保存配置的getters,如果没有则为{}
this.getters = {};
// 把getters中的属性定义到this.getters中,并且根据状态变化,重新执行此函数
Object.keys(getters).forEach((getterName) => {
Object.defineProperty(this.getters, getterName, {
get: () => {
return getters[getterName](this.state); // 通过getterName获取值的时候,执行getter函数并返回作为新的状态值
}
});
});
}
}
⑤ 接下来就是要处理mutations了,mutations相对较简单,我们只需要直接遍历mutations重新赋值即可,mutations使用的时候是通过commit方法,所以需要新增一个commit方法,并传递mutation的名称和数据,执行的时候,根据mutation名称找到对应的mutaion并执行,如:
class Store {
constructor(options) {
const mutations = options.mutations || {}; // 保存配置的mutations,如果没有则为{}
this.mutations = {};
Object.keys(mutations).forEach((mutationName) => {
// 将传递过来的mutation函数名添加到this.mutations对象中,
// 并赋值一个新的函数接收payload参数,函数内部执行对应的mutation
this.mutations[mutationName] = (payload) => {
mutations[mutationName].call(this, this.state, payload); //绑定this为Store对象,以便在mutation函数中this是Store对象
}
});
}
commit(type, payload) {
// 从this.mutations对象中根据type取出对应的mutation函数并传入参数执行
this.mutations[type](payload);
}
}
⑥ 接下来就是要处理actions了,actions同mutations一样,只不过是通过dispatch调用,所以需要新增一个dispatch方法,如:
class Store {
constructor(options) {
const actions = options.actions || {}; // 保存配置的actions,如果没有则为{}
this.mutations = {};
Object.keys(actions).forEach((actionName) => {
// 将传递过来的actions函数名添加到this.actions对象中,
// 并赋值一个新的函数接收payload参数,函数内部执行对应的action
this.actions[actionName] = (payload) => {
actions[actionName].call(this, this, payload); //绑定this为Store对象,以便在action函数中this是Store对象
}
});
}
dispatch(type, payload) {
// 从this.actions对象中根据type取出对应的action函数并传入参数执行
this.actions[type](payload);
}
}
此时还存在一个bug,那就是当执行actions里的asyncMinus时,里面的commit只是一个函数,没有对象直接调用,那么commit()函数执行的时候,其中的this将会变成window对象,但是由于Vue代码经babel打包后会自动加上"use strict",所以this将会变成undefined,将导致commit中获取mutation失败,所以我们必须重新定义commit和dispath方法,首先获取到原来的commit和dispatch方法,再调用的时候强制绑定为Store对象即可,如:
class Store {
constructor(options) {
const {commit, dispatch} = this; // 取出commit和dispatch函数
this.commit = (type, payload) => { // 对commit函数进行重新定义,并重新绑定this为Stroe对象
commit.call(this, type, payload);
}
this.dispatch = (type, payload) => { // 对dispatch函数进行重新定义,并重新绑定this为Stroe对象
dispatch.call(this, type, payload);
}
}
}
四、总结
vuex插件主要就是导出一个对象,由于vuex插件需要安装,所以需要有一个install方法,同时为了方便使用并作为一个全局对象,需要将Store对象注入到所有组件中,为了创建Store对象,需要对外提供一个Store类,其主要接收一个配置对象,里面包含state、getters、mutations、actions,然后分别对其进行处理,为了能够让state发生变化后能够在页面上更新,所以需要将state放到Vue实例对象中,即创建一个Vue实例对象,将state作为Vue实例对象的data;getters则是通过defineProperty重新定义,在获取值的时候进行拦截,执行getter函数;mutations和actions则是遍历并赋值为一个新函数即可,函数执行的时候,执行具体的mutation和action即可。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。