1 从Vuex的结构分析
我们先从Vuex的基本结构来分析
基本上我们就是在页面中通过state访问数据,然后通过commit和dispatch方法来更新数据。而当我们改变state中的数据时也会通知页面重新渲染,这个过程其实还是通过Vue来实现的。当我们的state是响应式数据时,数据更新就会通知页面进行更新。
所以,我们要做的就有一个大致的思路了。我们实现一个Store类,类中有一个响应式的state,然后有commit和dispatch方法。而响应的state其实我们可以通过new Vue({data: state})。
2 Vuex基本结构实现
接下我们就能来着手实现一下
// 存储Vue构造函数,避免直接引用Vue
let Vue
class Store {
constructor(options) {
//将state变为响应式数据,数据变化时直接通知页面更新
this.state = new Vue({
data: options.state
})
}
}
function install(_Vue) {
Vue = _Vue
// 通过混入寻找根实例进行全局挂载
Vue.mixin({
beforeCreate() {
if(this.$options.store) {
Vue.prototype.$store = this.$options.store
}
},
})
}
export default {
Store,
install
}
这里new Vue的data写法是因为在mian.js中才会将我们的store初始化,这里直接就是根实例,之前我们用data(){ reutrn }就是避免组件的data对全局进行污染,然而我们这里是根实例,不用考虑。
基本的state已经实现了,接下来我们就可以来着手实现两个方法了。
我们先从commit来,commit提交的mutations里的方法,所以我们要先获取到mutations,然后我们根据传入的方法名来找到对应的方法进行调用。
constructor(options) {
this._mutations = options.mutations
}
commit(type, payload) {
const entry = this._mutations[type]
if(entry) {
entry(this.state, payload)
}
}
dispatch也差不多
dispatch(type, payload) {
const entry = this._actions[type]
if(entry) {
entry(this, payload)
}
}
因为dispatch第一个参数接收的是一个store上下文,所以我们这里直接传一个this。
我们试着用一下。
import Vue from 'vue'
import Vuex from './cvuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
counter: 0
},
getters: {
doubleCounter(state) {
return state.counter * 2
}
},
mutations: {
add(state) {
state.counter++
// this.state
}
},
actions: {
// 结构上下文
add({ commit }) {
setTimeout(() => {
commit('add')
}, 1000);
}
}
})
这里报错显示_mutations未定义,那么就是说我们在commit中const entry = this._mutations[type]这一步出现了问题。而这里大概率出问题的地方就在于this的指向。
3 actions中this指向问题
我们来看看我们在actions中是怎么定义方法的。
actions: {
// 解构上下文
add({ commit }) {
setTimeout(() => {
commit('add')
}, 1000);
}
},
有没有发现,我们这里是直接解构commit并进行了commit的独立调用。我们知道函数的独立调用在非严格模式下this为window,而在严格模式下的this则为undefined。不管哪个模式,我们都无法在commit中通过this拿到mutations中的方法。
所以我们这里有两种解决方案。
-
不使用独立函数调用:
add(context) { setTimeout(() => { contextcommit('add') }, 1000); }不结构,直接拿到我们传入的this,进行隐式绑定拿到this
-
通过显示绑定
constructor(options) { ... this.commit = this.commit.bind(this) this.dispatch = this.dispatch.bind(this) }这样不管我们怎么调用这两个方法,我们都能拿到this。
4 隐藏state
现在就大致实现了一个vuex,但是Vuex不允许我们直接对state进行赋值。但是我们现在的cvuex是可以直接用$store.state.a = 1这种方式直接进行赋值。所以我们要进行 一点优化。
constructor(options) {
// this.state = new Vue({
// data: options.state
// })
this._vm = new Vue({
data: {
$$state: options.state
}
})
}
get state() {
console.log(this._vm);
return this._vm._data.$$state
}
在vue的data中我们用两个$定义的参数是不能用一般的方法进行访问。然后我们在写一个存取器返回state。
5 getters实现
我们好像还忘了getters的实现。
我们先从getters的使用来分析。
<p>double counter:{{$store.getters.doubleCounter}}</p>
在我们使用时,getters是直接在$store下获取的,所以,我们的getters应该是直接定义在构造器里面的而不是单独的方法。
我们再从定义来看
getters: {
doubleCounter(state) {
return state.counter * 2
}
}
我们定义的时候是直接定义不同的方法来获取。所以我们要做的就是在state中添加一个getters属性,但是我们用.xxx获取的时候需要调用这个方法并传入我们的state来获取返回值,换句话说就是需要将getters中的属性方法变为属性值。
这个时候我们就需要用上vue中的计算属性。
constructor(options) {
const getters = options.getters;
this.getters = {};
const computed = {};
for (const key in getters) {
if (Object.hasOwnProperty.call(getters, key)) {
const element = getters[key];
// 将每个属性方法都变为computed中的属性方法,这样就能直接通过计算属性获取属性值
computed[key] = () => {
return element(this.state)
}
}
}
this._vm = new Vue({
data: {
$$state: options.state,
},
computed,
});
}
现在我们就能通过创建的Vue实例直接访问属性来获取属性值。然后通过Object.defineProperty来重新设置对象属性。
for (const key in getters) {
if (Object.hasOwnProperty.call(getters, key)) {
const element = getters[key];
computed[key] = () => {
return element(this.state)
}
}
// 通过设置get直接获取vue实例中计算属性的值,进而将属性方法变为属性值
Object.defineProperty(this.getters, key, {
get: () => {
return this._vm[key]
}
})
}